diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..59c989e6 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,11 @@ +[target.x86_64-apple-darwin] +rustflags = [ + "-C", "link-arg=-undefined", + "-C", "link-arg=dynamic_lookup", +] + +[target.aarch64-apple-darwin] +rustflags = [ + "-C", "link-arg=-undefined", + "-C", "link-arg=dynamic_lookup", +] \ No newline at end of file diff --git a/.darglint b/.darglint deleted file mode 100644 index 01c1eab9..00000000 --- a/.darglint +++ /dev/null @@ -1,3 +0,0 @@ -# .darglint -[darglint] -strictness = short \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..c9e78c18 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,38 @@ +name: CI + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + test_and_build: + name: ${{ matrix.os }}_py${{ matrix.python_version }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, ubuntu-latest] + python_version: + ["3.9", "3.10", "3.11"] + defaults: + run: + shell: bash -l {0} + + steps: + - uses: actions/checkout@v3 + - uses: conda-incubator/setup-miniconda@v2 + with: + activate-environment: av2 + environment-file: conda/environment.yml + mamba-version: "*" + miniforge-version: latest + python-version: ${{ matrix.python_version }} + + - name: Build `av2`. + run: | + maturin develop --extras test + + - name: Run `pytest`. + run: | + pytest tests --cov src/av2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index ff39f09b..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: CI - -on: [push, pull_request] - -jobs: - test_and_build: - name: ${{ matrix.os }}_${{ matrix.venv_backend }}_py${{ matrix.python_version }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - python_version: - ["3.8", "3.9", "3.10"] - venv_backend: - ["virtualenv"] - defaults: - run: - shell: bash -l {0} - - steps: - - uses: actions/checkout@v2 - - uses: conda-incubator/setup-miniconda@v2 - with: - mamba-version: "*" - miniforge-version: latest - python-version: ${{ matrix.python_version }} - - - name: Install prerequisites. - run: | - mamba install -y nox pip pyyaml - - - name: Run nox with ${{ matrix.venv_backend }}. - run: | # Cache the environments (-r), fail on missing python interpreters (--error-on-missing-interpreters), set dependency resolver backend. - python -m nox -r --error-on-missing-interpreters --python ${{ matrix.python_version }} --default-venv-backend ${{ matrix.venv_backend }} - - - name: Install pypa/build. - run: >- - python -m - pip install - build - --user - - - name: Build a binary wheel and a source tarball. - run: >- - python -m - build - --sdist - --wheel - --outdir dist/ - . diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 00000000..fdc12461 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,71 @@ +# Based on https://github.com/PyO3/maturin/blob/main/.github/workflows/lint.yml. + +name: Lint + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + fmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain + with: + components: rustfmt + - name: cargo fmt + run: cargo fmt --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain + with: + components: clippy + - name: cargo clippy + run: cargo clippy --tests --all-features -- -D warnings + + black: + name: Black + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.x' + - uses: psf/black@stable + + ruff: + name: Ruff + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.x' + - run: pip install ruff + - run: ruff . + + mypy: + name: Mypy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.x' + - run: pip install mypy numpy + - run: mypy . + + # spell_check: + # name: Spellcheck + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + # - uses: codespell-project/actions-codespell@master diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..a4ee099a --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,130 @@ +name: "[av2] Release" + +on: + push: + branches: + - main + tags: + - '*' + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + linux: + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64, x86, aarch64] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: "true" + manylinux: auto + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + # windows: + # runs-on: windows-latest + # strategy: + # matrix: + # target: [x64, x86] + # steps: + # - uses: actions/checkout@v3 + # - uses: actions/setup-python@v4 + # with: + # python-version: "3.10" + # architecture: ${{ matrix.target }} + # - name: Build wheels + # uses: PyO3/maturin-action@v1 + # with: + # target: ${{ matrix.target }} + # args: --release --out dist --find-interpreter + # sccache: "true" + # - name: Upload wheels + # uses: actions/upload-artifact@v3 + # with: + # name: wheels + # path: dist + + macos: + runs-on: macos-latest + strategy: + matrix: + target: [x86_64, aarch64] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: "true" + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + # test_release: + # name: Test release. + # needs: [linux, windows, macos, sdist] + # runs-on: ubuntu-latest + # steps: + # - uses: actions/download-artifact@v3 + # with: + # name: wheels + # path: dist + # - name: Publish distribution 📦 to Test PyPI + # uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # password: ${{ secrets.TEST_PYPI_API_TOKEN }} + # repository-url: https://test.pypi.org/legacy/ + + release: + name: Release + runs-on: ubuntu-latest + if: "startsWith(github.ref, 'refs/tags/')" + needs: [linux, macos, sdist] + steps: + - uses: actions/download-artifact@v3 + with: + name: wheels + - name: Publish to PyPI + uses: PyO3/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + with: + command: upload + args: --skip-existing * diff --git a/.gitignore b/.gitignore index be38ce6a..3c74cb3d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +*.pyc # PyInstaller # Usually these files are written by a python script from a template @@ -153,6 +154,5 @@ condaenv* */**/.DS_Store experiments -*.parquet *.pt *.mp4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 97f02ea2..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,14 +0,0 @@ -repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 - hooks: - - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: local - hooks: - - id: nox - name: Run nox. - language: python - entry: nox -rs black isort mypy - pass_filenames: false diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..1f5f22e4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3032 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "approx" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" +dependencies = [ + "num-traits", +] + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "argminmax" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202108b46429b765ef483f8a24d5c46f48c14acfdacc086dd4ab6dddf6bcdbd2" +dependencies = [ + "ndarray", + "num-traits", +] + +[[package]] +name = "array-init-cursor" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7d0a018de4f6aa429b9d33d69edf69072b1c5b1cb8d3e4a5f7ef898fc3eb76" + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atoi_simd" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccfc14f5c3e34de57539a7ba9c18ecde3d9bbde48d232ea1da3e468adb307fd0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "av2" +version = "0.3.0" +dependencies = [ + "anyhow", + "argminmax", + "bincode", + "blas-src", + "criterion", + "dirs 4.0.0", + "env_logger", + "glob", + "ignore", + "image", + "indicatif", + "itertools", + "log", + "ndarray", + "nshare", + "numpy", + "once_cell", + "openblas-src", + "polars", + "pyo3", + "pyo3-polars", + "rand", + "rand_distr", + "rayon", + "serde", + "strum 0.24.1", + "strum_macros 0.24.3", +] + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] + +[[package]] +name = "blas-src" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb48fbaa7a0cb9d6d96c46bac6cedb16f13a10aebcef1c4e73515aaae8c9909d" +dependencies = [ + "openblas-src", +] + +[[package]] +name = "bstr" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cblas-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6feecd82cce51b0204cf063f0041d69f24ce83f680d87514b004248e7b0fa65" +dependencies = [ + "libc", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.48.5", +] + +[[package]] +name = "chrono-tz" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half 1.8.2", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "bitflags 1.3.2", + "clap_lex", + "indexmap 1.9.3", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "comfy-table" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c64043d6c7b7a4c58e39e7efccfdea7b93d885a795d0c054a69dbbf4dd52686" +dependencies = [ + "crossterm", + "strum 0.25.0", + "strum_macros 0.25.3", + "unicode-width", +] + +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.45.0", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c3242926edf34aec4ac3a77108ad4854bffaa2e4ddc1824124ce59231302d5" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9bcf5bdbfdd6030fb4a1c497b5d5fc5921aa2f60d359a17e249c0e6df3de153" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.1", + "crossterm_winapi", + "libc", + "parking_lot", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "enum_dispatch" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f33313078bb8d4d05a2733a94ac4c2d8a0df9a2b84424ebf4f33bfc224a890e" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "ethnum" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" + +[[package]] +name = "exr" +version = "1.71.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832a761f35ab3e6664babfbdc6cef35a4860e816ec3916dcfd0882954e98a8a8" +dependencies = [ + "bit_field", + "flume", + "half 2.2.1", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fast-float" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95765f67b4b18863968b4a1bd5bb576f732b29a4a28c7cd84c09fa3e2875f33c" + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fdeflate" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1b05cbd864bcaecbd3455d6d967862d446e4ebfc3c2e5e5b9841e53cba6673" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", + "rayon", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.51.1", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "ignore" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.24.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "qoi", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[package]] +name = "indicatif" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.3", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "itoap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8" + +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "lz4" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9e2dd86df36ce760a60f6ff6ad526f7ba1f14ba0356f8254fb6905e6494df1" +dependencies = [ + "libc", + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" +dependencies = [ + "autocfg", + "num_cpus", + "once_cell", + "rawpointer", + "thread-tree", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memmap2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "multiversion" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2c7b9d7fe61760ce5ea19532ead98541f6b4c495d87247aff9826445cf6872a" +dependencies = [ + "multiversion-macros", + "target-features", +] + +[[package]] +name = "multiversion-macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26a83d8500ed06d68877e9de1dde76c1dbb83885dcdbda4ef44ccbc3fbda2ac8" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "target-features", +] + +[[package]] +name = "nalgebra" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb2d0de08694bed883320212c18ee3008576bfe8c306f4c3c4a58b4876998be" +dependencies = [ + "approx 0.5.1", + "matrixmultiply", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "approx 0.4.0", + "cblas-sys", + "libc", + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", + "rayon", +] + +[[package]] +name = "now" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89e9874397a1f0a52fc1f197a8effd9735223cb2390e9dcc83ac6cd02923d0" +dependencies = [ + "chrono", +] + +[[package]] +name = "nshare" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4447657cd40e3107416ec4f2ac3e61a18781b00061789e3b8f4bbcbccb26c4c6" +dependencies = [ + "image", + "nalgebra", + "ndarray", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.3", + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "numpy" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef41cbb417ea83b30525259e30ccef6af39b31c240bda578889494c5392d331" +dependencies = [ + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", + "rustc-hash", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "openblas-build" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba42c395477605f400a8d79ee0b756cfb82abe3eb5618e35fa70d3a36010a7f" +dependencies = [ + "anyhow", + "flate2", + "native-tls", + "tar", + "thiserror", + "ureq", + "walkdir", +] + +[[package]] +name = "openblas-src" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e5d8af0b707ac2fe1574daa88b4157da73b0de3dc7c39fe3e2c0bb64070501" +dependencies = [ + "dirs 3.0.2", + "openblas-build", + "vcpkg", +] + +[[package]] +name = "openssl" +version = "0.10.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "planus" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1691dd09e82f428ce8d6310bd6d5da2557c82ff17694d2a32cad7242aea89f" +dependencies = [ + "array-init-cursor", +] + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polars" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e43795c49010cb851d45227caa17769e83760e21d260ba6285c563b754e1652f" +dependencies = [ + "getrandom", + "polars-core", + "polars-io", + "polars-lazy", + "polars-ops", + "polars-sql", + "polars-time", + "version_check", +] + +[[package]] +name = "polars-arrow" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faacd21a2548fa6d50c72d6b8d4649a8e029a0f3c6c5545b7f436f0610e49b0f" +dependencies = [ + "ahash", + "atoi", + "atoi_simd", + "bytemuck", + "chrono", + "chrono-tz", + "dyn-clone", + "either", + "ethnum", + "fast-float", + "foreign_vec", + "getrandom", + "hashbrown 0.14.3", + "itoa", + "itoap", + "lz4", + "multiversion", + "num-traits", + "polars-arrow-format", + "polars-error", + "polars-utils", + "ryu", + "serde", + "simdutf8", + "streaming-iterator", + "strength_reduce", + "version_check", + "zstd", +] + +[[package]] +name = "polars-arrow-format" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b0ef2474af9396b19025b189d96e992311e6a47f90c53cd998b36c4c64b84c" +dependencies = [ + "planus", + "serde", +] + +[[package]] +name = "polars-compute" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d9dc87f8003ae0edeef5ad9ac92b2a345480bbe17adad64496113ae84706dd" +dependencies = [ + "bytemuck", + "num-traits", + "polars-arrow", + "polars-error", + "polars-utils", + "version_check", +] + +[[package]] +name = "polars-core" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "befd4d280a82219a01035c4f901319ceba65998c594d0c64f9a439cdee1d7777" +dependencies = [ + "ahash", + "bitflags 2.4.1", + "bytemuck", + "chrono", + "chrono-tz", + "comfy-table", + "either", + "hashbrown 0.14.3", + "indexmap 2.1.0", + "ndarray", + "num-traits", + "once_cell", + "polars-arrow", + "polars-compute", + "polars-error", + "polars-row", + "polars-utils", + "rand", + "rand_distr", + "rayon", + "regex", + "serde", + "smartstring", + "thiserror", + "version_check", + "xxhash-rust", +] + +[[package]] +name = "polars-error" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f2435b02d1ba36d8c1f6a722cad04e4c0b2705a3112c5706e6960d405d7798" +dependencies = [ + "polars-arrow-format", + "regex", + "simdutf8", + "thiserror", +] + +[[package]] +name = "polars-io" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b51fba2cf014cb39c2b38353d601540fb9db643be65abb9ca8ff44b9c4c4a88e" +dependencies = [ + "ahash", + "atoi_simd", + "bytes", + "chrono", + "fast-float", + "home", + "itoa", + "memchr", + "memmap2", + "num-traits", + "once_cell", + "percent-encoding", + "polars-arrow", + "polars-core", + "polars-error", + "polars-time", + "polars-utils", + "rayon", + "regex", + "ryu", + "serde", + "simdutf8", + "smartstring", +] + +[[package]] +name = "polars-lazy" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83343e413346f048f3a5ad07c0ea4b5d0bada701a482878213142970b0ddff8" +dependencies = [ + "ahash", + "bitflags 2.4.1", + "glob", + "once_cell", + "polars-arrow", + "polars-core", + "polars-io", + "polars-ops", + "polars-pipe", + "polars-plan", + "polars-time", + "polars-utils", + "rayon", + "smartstring", + "version_check", +] + +[[package]] +name = "polars-ops" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6395f5fd5e1adf016fd6403c0a493181c1a349a7a145b2687cdf50a0d630310a" +dependencies = [ + "ahash", + "argminmax", + "base64", + "bytemuck", + "chrono", + "chrono-tz", + "either", + "hashbrown 0.14.3", + "hex", + "indexmap 2.1.0", + "memchr", + "num-traits", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-error", + "polars-utils", + "rayon", + "regex", + "serde", + "smartstring", + "unicode-reverse", + "version_check", +] + +[[package]] +name = "polars-pipe" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390a831b864bc57a4cb260b0595030dfb6a4260a3723cf8ca17968ee2078b8ff" +dependencies = [ + "crossbeam-channel", + "crossbeam-queue", + "enum_dispatch", + "hashbrown 0.14.3", + "num-traits", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-io", + "polars-ops", + "polars-plan", + "polars-row", + "polars-utils", + "rayon", + "smartstring", + "version_check", +] + +[[package]] +name = "polars-plan" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb7d7527be2aa33baace9000f6772eb9df7cd57ec010a4b273435d2dc1349e8" +dependencies = [ + "ahash", + "bytemuck", + "chrono-tz", + "once_cell", + "percent-encoding", + "polars-arrow", + "polars-core", + "polars-io", + "polars-ops", + "polars-time", + "polars-utils", + "rayon", + "regex", + "serde", + "smartstring", + "strum_macros 0.25.3", + "version_check", +] + +[[package]] +name = "polars-row" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4984d97aad3d0db92afe76ebcab10b5e37a1216618b5703ae0d2917ccd6168c" +dependencies = [ + "polars-arrow", + "polars-error", + "polars-utils", +] + +[[package]] +name = "polars-sql" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f62a8b8f93146ec1eb2ef340d77eeb174e8010035e449bfdd424d2b1fd944a" +dependencies = [ + "hex", + "polars-arrow", + "polars-core", + "polars-error", + "polars-lazy", + "polars-plan", + "rand", + "serde", + "serde_json", + "sqlparser", +] + +[[package]] +name = "polars-time" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d75348a51d0c97f3b83df860ecb35a6ac6c5dafc6278cac4e1ac101d96dc753" +dependencies = [ + "atoi", + "chrono", + "chrono-tz", + "now", + "once_cell", + "polars-arrow", + "polars-core", + "polars-error", + "polars-ops", + "polars-utils", + "regex", + "serde", + "smartstring", +] + +[[package]] +name = "polars-utils" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f9c955bb1e9b55d835aeb7fe4e4e8826e01abe5f0ada979ceb7d2b9af7b569" +dependencies = [ + "ahash", + "bytemuck", + "hashbrown 0.14.3", + "indexmap 2.1.0", + "num-traits", + "once_cell", + "polars-error", + "rayon", + "smartstring", + "sysinfo", + "version_check", +] + +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a89dc7a5850d0e983be1ec2a463a171d20990487c3cfcd68b5363f1ee3d6fe0" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07426f0d8fe5a601f26293f300afd1a7b1ed5e78b2a705870c5f30893c5163be" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb7dec17e17766b46bca4f1a4215a85006b4c2ecde122076c562dd058da6cf1" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f738b4e40d50b5711957f142878cfa0f28e054aa0ebdfc3fd137a843f74ed3" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc910d4851847827daf9d6cdd4a823fbdaab5b8818325c5e97a86da79e8881f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "pyo3-polars" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fa311764163c831c75f9ca49499abacbf7ece676cad0b059d962a384aa18224" +dependencies = [ + "polars", + "polars-core", + "pyo3", + "thiserror", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "safe_arch" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f398075ce1e6a179b46f51bd88d0598b92b00d3551f1a2d4ac49e771b56ac354" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "simba" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3fd720c48c53cace224ae62bef1bbff363a70c68c4802a78b5cc6159618176" +dependencies = [ + "approx 0.5.1", + "num-complex", + "num-traits", + "paste", + "wide", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "serde", + "static_assertions", + "version_check", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlparser" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743b4dc2cbde11890ccb254a8fc9d537fa41b36da00de2a1c5e9848c9bc42bd7" +dependencies = [ + "log", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.41", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sysinfo" +version = "0.30.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb4f3438c8f6389c864e61221cbc97e9bca98b4daf39a5beb7bea660f528bb2" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "windows", +] + +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-features" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb5fa503293557c5158bd215fdc225695e567a77e453f5d4452a50a193969bd" + +[[package]] +name = "target-lexicon" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "termcolor" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "thread-tree" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbd370cb847953a25954d9f63e14824a36113f8c72eecf6eccef5dc4b45d630" +dependencies = [ + "crossbeam-channel", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-reverse" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bea5dacebb0d2d0a69a6700a05b59b3908bf801bf563a49bd27a1b60122962c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + +[[package]] +name = "ureq" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" +dependencies = [ + "base64", + "flate2", + "log", + "native-tls", + "once_cell", + "rustls-native-certs", + "url", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.41", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "wide" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68938b57b33da363195412cfc5fc37c9ed49aa9cfe2156fde64b8d2c9498242" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "xattr" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7dae5072fe1f8db8f8d29059189ac175196e410e40ba42d5d4684ae2f750995" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9828b178da53440fa9c766a3d2f73f7cf5d0ac1fe3980c1e5018d899fd19e07b" + +[[package]] +name = "zerocopy" +version = "0.7.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "zstd" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.9+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..d50b2af8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["rust"] +resolver = "2" diff --git a/DOWNLOAD.md b/DOWNLOAD.md deleted file mode 100644 index 94432650..00000000 --- a/DOWNLOAD.md +++ /dev/null @@ -1,38 +0,0 @@ -# Downloading the Argoverse 2 Datasets -Our datasets are available for download from [AWS S3](https://aws.amazon.com/s3/). For the best experience, we highly recommend using the open-source [s5cmd](https://github.com/peak/s5cmd) tool to transfer the data to your local filesystem (additional info available [here](https://aws.amazon.com/blogs/opensource/parallelizing-s3-workloads-s5cmd/)). Please note that an AWS account is not required to download the datasets. - -### Installing `s5cmd` - -`s5cmd` can be easily installed with the following script: - -```bash -#!/usr/bin/env bash - -export INSTALL_DIR=$HOME/.local/bin -export PATH=$PATH:$INSTALL_DIR -export S5CMD_URI=https://github.com/peak/s5cmd/releases/download/v1.4.0/s5cmd_1.4.0_$(uname | sed 's/Darwin/macOS/g')-64bit.tar.gz - -mkdir -p $INSTALL_DIR -curl -sL $S5CMD_URI | tar -C $INSTALL_DIR -xvzf - s5cmd -``` - -Note that it will install `s5cmd` in your local bin directory. You can always change the path if you prefer installing it in another directory. - -### Downloading Datasets -Once `s5cmd` is installed installed, downloading a dataset is as easy as running the following (using the sensor dataset as an example): - -```bash -s5cmd --no-sign-request cp s3://argoai-argoverse/av2/sensor/* target-directory -``` - -The command will download all S3 objects to the target directory (for example, `target-directory` can be `/home/av2/sensors/`). Given the size of the dataset, it might take a couple of hours depending on the network connectivity. - -When the download is finished, the dataset is ready to use! - -### Dataset S3 Locations -```bash -s3://argoai-argoverse/av2/sensor/ -s3://argoai-argoverse/av2/lidar/ -s3://argoai-argoverse/av2/motion-forecasting/ -s3://argoai-argoverse/av2/tbv/ -``` diff --git a/README.md b/README.md index 6444b23b..9e07caaa 100644 --- a/README.md +++ b/README.md @@ -2,121 +2,39 @@ ![CI Status](https://github.com/argoai/av2-api/actions/workflows/ci.yml/badge.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) -# Argoverse 2 API +# Argoverse 2 > _Official_ GitHub repository for the [Argoverse 2](https://www.argoverse.org) family of datasets. -If you have any questions or run into any problems with either the data or API, please feel free to open a [GitHub issue](https://github.com/argoai/av2-api/issues)! - -## Announcements - -### Argoverse competitions are live! - - Argoverse 2 - - 3D Object Detection - - Challenge Link: https://eval.ai/challenge/1710/overview - - Baseline: https://github.com/benjaminrwilson/torchbox3d - - Motion Forecasting - - Challenge Link: https://eval.ai/challenge/1719/overview - - Argoverse 1 - - Stereo - - Challenge Link: https://eval.ai/challenge/1704/overview - -## TL;DR - -- Install the API: `pip install av2` -- Read the [instructions](DOWNLOAD.md) to download the data. - -## Overview - -- [Setup](#setup) -- [Datasets](#datasets) -- [Testing](#testing) -- [Contributing](#contributing) -- [Citing](#citing) -- [License](#license) +

+ +

## Getting Started -### Setup - -The easiest way to install the API is via [pip](https://pypi.org/project/av2/) by running the following command: - -```bash -pip install av2 -``` - -### Datasets - -The _Argoverse 2_ family consists of **four** distinct datasets: - -| Dataset Name | Scenarios | Camera Imagery | Lidar| Maps | Additional Information| -| ---------------| --------: | :------------: | :--: | :--: | :--------------------:| -| Sensor | 1,000 | :white_check_mark: | :white_check_mark: | :white_check_mark: | [Sensor Dataset README](src/av2/datasets/sensor/README.md) | -| Lidar | 20,000 | | :white_check_mark: | :white_check_mark: | [Lidar Dataset README](src/av2/datasets/lidar/README.md) | -| Motion Forecasting | 250,000 | | | :white_check_mark: | [Motion Forecasting Dataset README](src/av2/datasets/motion_forecasting/README.md) | -| Map Change (Trust, but Verify) | 1,045 | :white_check_mark: | :white_check_mark: | :white_check_mark: | [Map Change Dataset README](src/av2/datasets/tbv/README.md) | +Please see the [Argoverse User Guide](https://argoverse.github.io/user-guide/). -Please see [DOWNLOAD.md](DOWNLOAD.md) for detailed instructions on how to download each dataset. +## Supported Datasets -
-

Sensor Dataset

- - - - -
+- Argoverse 2 (AV2) -
-

Lidar Dataset

- - - - -
+ - [Sensor](https://argoverse.github.io/user-guide/datasets/sensor.html) + - [Lidar](https://argoverse.github.io/user-guide/datasets/lidar.html) + - [Motion Forecasting](https://argoverse.github.io/user-guide/datasets/motion_forecasting.html) + +- Trust, but Verify (TbV) + - [Map Change Detection](https://argoverse.github.io/user-guide/datasets/map_change_detection.html) +## Supported Tasks -
-

Motion Forecasting Dataset

- - - -
+- Argoverse 2 (AV2) -
-

Map Change Dataset (Trust, but Verify)

- - - - -
+ - [3D Object Detection](https://argoverse.github.io/user-guide/tasks/3d_object_detection.html) + - [3D Scene Flow](https://argoverse.github.io/user-guide/tasks/3d_scene_flow.html) + - [4D Occupancy Forecasting](https://argoverse.github.io/user-guide/tasks/4d_occupancy_forecasting.html) + - [End-to-End Forecasting](https://argoverse.github.io/user-guide/tasks/e2e_forecasting.html) + - [Motion Forecasting](https://argoverse.github.io/user-guide/tasks/motion_forecasting.html) -### Map API - -Please refer to the [map README](src/av2/map/README.md) for additional details about the common format for vector and -raster maps that we employ across all AV2 datasets. - -## Compatibility Matrix - -| `Python Version` | `linux` | `macOS` | `windows` | -| ------------- | :----------------: | :----------------: | :----------------: | -| `3.8` | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| `3.9` | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| `3.10` | :white_check_mark: | :white_check_mark: | :white_check_mark: | - -## Testing - -All incoming pull requests are tested using [nox](https://nox.thea.codes/en/stable/) as -part of the CI process. This ensures that the latest version of the API is always stable on all supported platforms. You -can run the full suite of automated checks and tests locally using the following command: - -```bash -nox -r -``` - -## Contributing - -Have a cool feature you'd like to add? Found an unhandled corner case? The Argoverse team welcomes contributions from -the open source community - please open a PR using the following [template](.github/pull_request_template.md)! ## Citing @@ -131,7 +49,7 @@ Please use the following citation when referencing the [Argoverse 2](https://dat } ``` -Use the following citation when referencing the [Argoverse 2](https://datasets-benchmarks-proceedings.neurips.cc/paper/2021/file/6f4922f45568161a8cdf4ad2299f6d23-Paper-round2.pdf) _Map Change_ Dataset: +Use the following citation when referencing the [Trust, but Verify](https://datasets-benchmarks-proceedings.neurips.cc/paper/2021/file/6f4922f45568161a8cdf4ad2299f6d23-Paper-round2.pdf) _Map Change Detection_ Dataset: ```BibTeX @INPROCEEDINGS { TrustButVerify, author = {John Lambert and James Hays}, @@ -140,8 +58,3 @@ Use the following citation when referencing the [Argoverse 2](https://datasets-b year = {2021} } ``` - -## License - -All code provided within this repository is released under the **MIT license** and bound by the _Argoverse_ **terms of use**, -please see [LICENSE](LICENSE) and [NOTICE](NOTICE) for additional details. diff --git a/conda/environment.yml b/conda/environment.yml index 23a11300..6882c848 100644 --- a/conda/environment.yml +++ b/conda/environment.yml @@ -1,18 +1,30 @@ name: av2 channels: + - pytorch - conda-forge dependencies: - av - click - joblib + - kornia - matplotlib-base + - maturin - nox - numba - - numpy>=1.21.5 + - numpy + - openblas - opencv + - openssl - pandas - pip + - polars - pyarrow - pyproj - rich + - rust - scipy + - pytorch + - tqdm + - universal_pathlib + - pip: + - git+https://github.com/JonathonLuiten/TrackEval.git diff --git a/conda/install.sh b/conda/install.sh index 730c393c..3b7619c6 100644 --- a/conda/install.sh +++ b/conda/install.sh @@ -10,4 +10,4 @@ conda install -y mamba -c conda-forge mamba env create -f ${SCRIPT_DIR}/${ENVIRONMENT_FILE} \ && eval "$(conda shell.bash hook)" \ && conda activate av2 \ -&& python -m pip install -e $SCRIPT_DIR/.. +&& OPENSSL_DIR=$CONDA_PREFIX python -m pip install -e $SCRIPT_DIR/.. diff --git a/noxfile.py b/noxfile.py deleted file mode 100644 index b1ed5d00..00000000 --- a/noxfile.py +++ /dev/null @@ -1,121 +0,0 @@ -# - -"""Test automation using `nox`.""" - -from pathlib import Path -from typing import Dict, Final, List, Union - -import nox -import yaml -from nox import Session -from nox.virtualenv import CondaEnv - -PYTHON: Final[List[str]] = ["3.8", "3.9", "3.10"] - -nox.options.sessions = ["black", "isort", "lint", "mypy", "pytest"] - - -def _setup(session: Session) -> None: - """Install `av2` into a virtual environment. - - Args: - session: `nox` session. - """ - if isinstance(session.virtualenv, CondaEnv): - - # Load environment.yml file and install conda dependencies. - env = yaml.safe_load(Path("conda/environment.yml").read_text()) - - conda_pkgs: List[str] = [] - reqs: List[str] = [] - pkgs: List[Union[str, Dict[str, str]]] = env["dependencies"] - for pkg in pkgs: - if isinstance(pkg, dict): - if "pip" in pkg: - reqs += pkg["pip"] - else: - conda_pkgs.append(pkg) - session.conda_install(*conda_pkgs) - - # Install pip dependencies if they exist. - if len(reqs) > 0: - session.install(*reqs, "--no-deps") - - # Install package. - session.install("-e", ".") - - -@nox.session(python=PYTHON) -def black(session: Session) -> None: - """Run `black` against `av2`. - - Args: - session: `nox` session. - """ - env = ["black[jupyter]"] - _setup(session) - session.install(*env) - session.run("black", ".") - - -@nox.session(python=PYTHON) -def isort(session: Session) -> None: - """Run `isort` against `av2`. - - Args: - session: `nox` session. - """ - env = ["isort"] - _setup(session) - session.install(*env) - session.run("isort", ".") - - -@nox.session(python=PYTHON) -def lint(session: Session) -> None: - """Lint using flake8.""" - env = [ - "flake8", - "flake8-annotations", - "flake8-black", - "flake8-bugbear", - "flake8-docstrings", - "flake8-import-order", - "darglint", - ] - _setup(session) - session.install(*env) - session.run("flake8", ".") - - -@nox.session(python=PYTHON) -def mypy(session: Session) -> None: - """Run `mypy` against `av2`. - - Args: - session: `nox` session. - """ - env = [ - "mypy", - "types-pyyaml", - ] - _setup(session) - session.install(*env) - session.run("mypy", ".") - - -@nox.session(python=PYTHON) -def pytest(session: Session) -> None: - """Run `pytest` against `av2`. - - Args: - session: `nox` session. - """ - env = [ - "pytest", - "pytest-benchmark", - "pytest-cov", - ] - _setup(session) - session.install(*env) - session.run("pytest", "tests", "--cov", "src/av2") diff --git a/pyproject.toml b/pyproject.toml index 322104c3..9fe9be79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,24 +1,95 @@ [build-system] -requires = [ - "setuptools>=42", - "wheel" +requires = ["maturin>=0.14,<0.15"] +build-backend = "maturin" + +[project] +name = "av2" +description = "Argoverse 2: Next generation datasets for self-driving perception and forecasting." +requires-python = ">=3.8" +license = { file = "LICENSE" } +keywords = [ + "argoverse", + "argoverse2", + "autonomous-driving", + "av1", + "av2", + "3d-object-detection", + "3d-scene-flow", + "4d-occupancy-forecasting", + "e2e-forecasting", + "motion-forecasting", +] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Rust", +] + +dependencies = [ + "av", + "click", + "joblib", + "kornia", + "matplotlib", + "nox", + "numba", + "numpy", + "opencv-python", + "pandas", + "polars", + "pyarrow", + "pyproj", + "rich", + "scipy", + "torch", + "tqdm", + "universal_pathlib", +] + +dynamic = [ + "authors", + "version" ] -build-backend = "setuptools.build_meta" -[tool.black] -line-length = 120 +readme = "README.md" -[tool.isort] -known_first_party = "av2" -known_third_party = "" +[project.optional-dependencies] +lint = [ + "black[jupyter]", + "mypy", + "ruff", +] +test = [ + "pytest", + "pytest-benchmark", + "pytest-cov", +] + +[project.urls] +homepage = "https://argoverse.org" +repository = "https://github.com/argoverse/av2-api" + +[tool.maturin] +features = ["pyo3/extension-module"] +module-name = "av2._r" + +[tool.ruff] +select = [ + "D", +] -line_length = 120 -multi_line_output = 3 -profile = "black" +[tool.ruff.pydocstyle] +convention = "google" [tool.mypy] exclude = "build" ignore_missing_imports = true +disallow_untyped_calls = false disallow_untyped_decorators = false plugins = "numpy.typing.mypy_plugin" strict = true @@ -33,6 +104,4 @@ reportUntypedFunctionDecorator = false [tool.pytest.ini_options] minversion = "6.0" addopts = "--cov-report term-missing:skip-covered --cov av2" -testpaths = [ - "tests", -] \ No newline at end of file +testpaths = ["tests"] diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 00000000..51dba9ed --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,68 @@ +[package] +name = "av2" +version = "0.3.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "av2" +crate-type = ["cdylib", "lib"] + +[dependencies] +anyhow = "1.0.66" +argminmax = { version = "0.6.1", features = [ + "ndarray", +], default-features = false } +blas-src = { version = "0.8", optional = true } +bincode = "1.3.3" +dirs = "4.0.0" +env_logger = "0.10.0" +glob = "0.3.1" +log = "0.4.17" +ignore = "0.4.20" +image = { version = "0.24.8" } +indicatif = "0.17.3" +itertools = "0.10.5" +ndarray = { version = "0.15.6", features = [ + "approx", + "matrixmultiply-threading", + "rayon", +] } +nshare = { version = "0.9.0", features = ["ndarray"] } +numpy = { version = "0.20.0" } +once_cell = "1.17.1" +openblas-src = { version = "0.10.8", optional = true } +polars = { version = "0.37.0", features = [ + "asof_join", + "dtype-u8", + "dtype-u16", + "lazy", + "ndarray", + "ipc", + "serde", + "serde-lazy", +] } +pyo3 = { version = "0.20.2", features = ["extension-module"] } +pyo3-polars = "0.11.3" +rand = "0.8.5" +rand_distr = "0.4.3" +rayon = "1.7.0" +serde = "1.0.160" +strum = "0.24.1" +strum_macros = "0.24.3" + +[dev-dependencies] +criterion = { version = "0.4", features = ["html_reports"] } + +[[bench]] +name = "benchmark" +harness = false + +[features] +blas = [ + "blas-src/openblas", + "ndarray/blas", + "ndarray/rayon", + "openblas-src/cblas", + "openblas-src/system", +] diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 00000000..6ddaabdf --- /dev/null +++ b/rust/README.md @@ -0,0 +1,3 @@ +# Rust API + +This API is **experimental** and is not intended to be used directly at this time. diff --git a/rust/benches/benchmark.rs b/rust/benches/benchmark.rs new file mode 100644 index 00000000..75b1570a --- /dev/null +++ b/rust/benches/benchmark.rs @@ -0,0 +1,52 @@ +//! # benchmark +//! +//! Benchmarking suite. + +use av2::{ + geometry::polytope::{compute_interior_points_mask, cuboids_to_polygons}, + io::{ndarray_from_frame, read_feather_eager}, +}; +use criterion::{criterion_group, criterion_main, Criterion}; +use once_cell::sync::Lazy; +use polars::{ + lazy::dsl::{col, cols, lit}, + prelude::IntoLazy, +}; +use std::{env::current_dir, path::PathBuf}; + +static TEST_DATA_DIR: Lazy = Lazy::new(|| { + current_dir() + .unwrap() + .join("../tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76") +}); + +fn io_benchmark(c: &mut Criterion) { + let path = current_dir().unwrap().join("../tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/sensors/lidar/315973157959879000.feather"); + c.bench_function("read_feather_eager", |b| { + b.iter(|| read_feather_eager(&path, false)) + }); +} + +fn geometry_benchmark(c: &mut Criterion) { + let timestamp_ns = 315973157959879000_u64; + let annotations_path = TEST_DATA_DIR.join("annotations.feather"); + let annotations = read_feather_eager(&annotations_path, false) + .lazy() + .filter(col("timestamp_ns").eq(lit(timestamp_ns))) + .collect() + .unwrap(); + let columns = [ + "tx_m", "ty_m", "tz_m", "length_m", "width_m", "height_m", "qw", "qx", "qy", "qz", + ]; + let annotations_ndarray = ndarray_from_frame(&annotations, cols(columns)); + let cuboid_vertices = cuboids_to_polygons(&annotations_ndarray.view()); + let lidar_path = TEST_DATA_DIR.join(format!("sensors/lidar/{timestamp_ns}.feather")); + let lidar = read_feather_eager(&lidar_path, false); + let lidar_ndarray = ndarray_from_frame(&lidar, cols(["x", "y", "z"])); + c.bench_function("compute_interior_points_mask", |b| { + b.iter(|| compute_interior_points_mask(&lidar_ndarray.view(), &cuboid_vertices.view())) + }); +} + +criterion_group!(benches, io_benchmark, geometry_benchmark); +criterion_main!(benches); diff --git a/rust/src/bin/export_accumulated_sweeps.rs b/rust/src/bin/export_accumulated_sweeps.rs new file mode 100644 index 00000000..88c8e760 --- /dev/null +++ b/rust/src/bin/export_accumulated_sweeps.rs @@ -0,0 +1,74 @@ +//! # build_accumulated_sweeps +//! +//! Accumulates lidar sweeps across a contiguous temporal window. +//! Defaults to a 5 frame window (~1 second of lidar data). + +#[cfg(feature = "blas")] +extern crate blas_src; +#[macro_use] +extern crate log; + +use std::{fs, path::PathBuf}; + +use av2::{data_loader::DataLoader, io::write_feather_eager}; +use indicatif::ProgressBar; + +use once_cell::sync::Lazy; + +/// Constants can be changed to fit your directory structure. +/// However, it's recommend to place the datasets in the default folders. + +/// Root directory to datasets. +static ROOT_DIR: Lazy = Lazy::new(|| dirs::home_dir().unwrap().join("data/datasets/")); + +/// Dataset name. +static DATASET_NAME: &str = "av2"; + +/// Dataset type. This will either be "lidar" or "sensor". +static DATASET_TYPE: &str = "sensor"; + +/// Split names for the dataset. +static SPLIT_NAMES: Lazy> = Lazy::new(|| vec!["train", "val", "test"]); + +/// Number of accumulated sweeps. +const NUM_ACCUMULATED_SWEEPS: usize = 5; + +/// Memory maps the sweeps for fast pre-processing. Requires .feather files to be uncompressed. +const MEMORY_MAPPED: bool = false; + +static DST_DATASET_NAME: Lazy = + Lazy::new(|| format!("{DATASET_NAME}_{NUM_ACCUMULATED_SWEEPS}_sweep")); +static SRC_PREFIX: Lazy = Lazy::new(|| ROOT_DIR.join(DATASET_NAME).join(DATASET_TYPE)); +static DST_PREFIX: Lazy = + Lazy::new(|| ROOT_DIR.join(DST_DATASET_NAME.clone()).join(DATASET_TYPE)); + +/// Script entrypoint. +pub fn main() { + env_logger::init(); + for split_name in SPLIT_NAMES.clone() { + if !SRC_PREFIX.join(split_name).exists() { + error!("Cannot find `{split_name}` split. Skipping ..."); + continue; + } + let data_loader = DataLoader::new( + ROOT_DIR.clone().to_str().unwrap(), + DATASET_NAME, + DATASET_TYPE, + split_name, + NUM_ACCUMULATED_SWEEPS, + MEMORY_MAPPED, + ); + let bar = ProgressBar::new(data_loader.len() as u64); + for sweep in data_loader { + let lidar = sweep.lidar.0; + let (log_id, timestamp_ns) = sweep.sweep_uuid; + + let suffix = format!("{split_name}/{log_id}/sensors/lidar/{timestamp_ns}.feather"); + let dst = DST_PREFIX.clone().join(suffix); + + fs::create_dir_all(dst.parent().unwrap()).unwrap(); + write_feather_eager(&dst, lidar); + bar.inc(1) + } + } +} diff --git a/rust/src/bin/export_augmentation_database.rs b/rust/src/bin/export_augmentation_database.rs new file mode 100644 index 00000000..cacc5bb3 --- /dev/null +++ b/rust/src/bin/export_augmentation_database.rs @@ -0,0 +1,172 @@ +//! # export_augmentation_database +//! +//! Exports cropped annotations from the `train` dataset for augmentation during training. + +#[cfg(feature = "blas")] +extern crate blas_src; +#[macro_use] +extern crate log; + +use std::{fs, path::PathBuf}; + +use av2::{ + data_loader::DataLoader, + geometry::polytope::{compute_interior_points_mask, cuboids_to_polygons}, + io::write_feather_eager, +}; +use indicatif::ProgressBar; + +use itertools::Itertools; +use ndarray::{s, Array, Axis, Ix2}; +use once_cell::sync::Lazy; +use polars::{ + df, + prelude::{DataFrame, Float32Type, IndexOrder, NamedFrom}, + series::Series, +}; +use std::collections::HashMap; + +/// Constants can be changed to fit your directory structure. +/// However, it's recommend to place the datasets in the default folders. + +/// Root directory to datasets. +static ROOT_DIR: Lazy = Lazy::new(|| dirs::home_dir().unwrap().join("data/datasets/")); + +/// Dataset name. +static DATASET_NAME: &str = "av2"; + +/// Dataset type. This will either be "lidar" or "sensor". +static DATASET_TYPE: &str = "sensor"; + +/// Split names for the dataset. +static SPLIT_NAMES: Lazy> = Lazy::new(|| vec!["train"]); + +/// Number of accumulated sweeps. +const NUM_ACCUMULATED_SWEEPS: usize = 1; + +/// Memory maps the sweeps for fast pre-processing. Requires .feather files to be uncompressed. +const MEMORY_MAPPED: bool = false; + +static DST_DATASET_NAME: Lazy = + Lazy::new(|| format!("{DATASET_NAME}_{NUM_ACCUMULATED_SWEEPS}_database")); +static SRC_PREFIX: Lazy = Lazy::new(|| ROOT_DIR.join(DATASET_NAME).join(DATASET_TYPE)); +static DST_PREFIX: Lazy = + Lazy::new(|| ROOT_DIR.join(DST_DATASET_NAME.clone()).join(DATASET_TYPE)); + +static EXPORTED_COLUMN_NAMES: Lazy> = Lazy::new(|| { + vec![ + "x", + "y", + "z", + "intensity", + "laser_number", + "offset_ns", + "timedelta_ns", + ] +}); + +/// Script entrypoint. +pub fn main() { + env_logger::init(); + for split_name in SPLIT_NAMES.clone() { + let split_path = SRC_PREFIX.join(split_name); + if !split_path.exists() { + error!("Cannot find `{split_path:?}`. Skipping ..."); + continue; + } + let data_loader = DataLoader::new( + ROOT_DIR.clone().to_str().unwrap(), + DATASET_NAME, + DATASET_TYPE, + split_name, + NUM_ACCUMULATED_SWEEPS, + MEMORY_MAPPED, + ); + + let mut category_counter: HashMap = HashMap::new(); + let bar = ProgressBar::new(data_loader.len() as u64); + for sweep in data_loader { + let lidar = &sweep.lidar.0; + let lidar_ndarray = lidar.to_ndarray::(IndexOrder::C).unwrap(); + + let cuboids = sweep.cuboids.unwrap().0; + let category = cuboids["category"] + .utf8() + .unwrap() + .into_iter() + .map(|x| x.unwrap()) + .collect_vec() + .clone(); + + let cuboids = cuboids + .clone() + .to_ndarray::(IndexOrder::C) + .unwrap(); + let cuboid_vertices = cuboids_to_polygons(&cuboids.view()); + let points = lidar_ndarray.slice(s![.., ..3]); + let mask = compute_interior_points_mask(&points.view(), &cuboid_vertices.view()); + for (c, m) in category.into_iter().zip(mask.outer_iter()) { + let indices = m + .iter() + .enumerate() + .filter_map(|(i, x)| match *x { + true => Some(i), + _ => None, + }) + .collect_vec(); + + let points_i = lidar_ndarray.select(Axis(0), &indices); + let data_frame_i = _build_data_frame(points_i, EXPORTED_COLUMN_NAMES.clone()); + + category_counter + .entry(c.to_string()) + .and_modify(|count| *count += 1) + .or_insert(0); + let count = category_counter.get(&c.to_string()).unwrap(); + + let dst = DST_PREFIX.join(c).join(format!("{count:08}.feather")); + fs::create_dir_all(dst.parent().unwrap()).unwrap(); + write_feather_eager(&dst, data_frame_i); + } + bar.inc(1); + } + + let category = category_counter.keys().cloned().collect_vec(); + let count = category_counter.values().cloned().collect_vec(); + let num_padding = category_counter.values().map(|_| 8_u8).collect_vec(); + let index = + df!("category" => category, "count" => count, "num_padding" => num_padding).unwrap(); + + let dst = DST_PREFIX.join("_index.feather"); + write_feather_eager(&dst, index); + } +} + +// Helper method to build exported `DataFrame`. +fn _build_data_frame(arr: Array, column_names: Vec<&str>) -> DataFrame { + let series_vec = arr + .columns() + .into_iter() + .zip(column_names) + .map(|(column, column_name)| match column_name { + "x" => Series::new("x", column.to_owned().into_raw_vec()), + "y" => Series::new("y", column.to_owned().into_raw_vec()), + "z" => Series::new("z", column.to_owned().into_raw_vec()), + "intensity" => Series::new( + "intensity", + column.to_owned().mapv(|x| x as u8).into_raw_vec(), + ), + "laser_number" => Series::new( + "laser_number", + column.to_owned().mapv(|x| x as u8).into_raw_vec(), + ), + "offset_ns" => Series::new( + "offset_ns", + column.to_owned().mapv(|x| x as u32).into_raw_vec(), + ), + "timedelta_ns" => Series::new("timedelta_ns", column.to_owned().into_raw_vec()), + _ => panic!(), + }) + .collect_vec(); + DataFrame::from_iter(series_vec) +} diff --git a/rust/src/constants.rs b/rust/src/constants.rs new file mode 100644 index 00000000..b54bcceb --- /dev/null +++ b/rust/src/constants.rs @@ -0,0 +1,116 @@ +//! # constants +//! +//! Common constants used throughout the library. + +use strum_macros::{Display, EnumIter, EnumString}; + +/// Annotation dataframe columns. +/// Found in `annotations.feather`. +pub const ANNOTATION_COLUMNS: [&str; 13] = [ + "tx_m", + "ty_m", + "tz_m", + "length_m", + "width_m", + "height_m", + "qw", + "qx", + "qy", + "qz", + "num_interior_pts", + "category", + "track_uuid", +]; + +/// Pose dataframe columns. +/// Found in `city_SE3_egovehicle`. +pub const POSE_COLUMNS: [&str; 7] = ["tx_m", "ty_m", "tz_m", "qw", "qx", "qy", "qz"]; + +/// Unknown map file name for use if the map doesn't exist. +pub const DEFAULT_MAP_FILE_NAME: &str = "log_map_archive___DEFAULT_city_00000.json"; + +/// Argoverse Sensor dataset categories. +#[derive(Clone, Copy, Debug, EnumIter, EnumString, PartialEq)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +pub enum AV2Categories { + /// All recognized animals large enough to affect traffic, but that do not fit into the Cat, Dog, or Horse categories. + Animal, + /// Articulated buses perform the same function as a standard city bus, but are able to bend (articulate) towards the center. These will also have a third set of wheels not present on a typical bus. + ArticulatedBus, + /// Non-motorized vehicle that typically has two wheels and is propelled by human power pushing pedals in a circular motion. + Bicycle, + /// Person actively riding a bicycle, non-pedaling passengers included. + Bicyclist, + /// Bollards are short, sturdy posts installed in the roadway or sidewalk to control the flow of traffic. These may be temporary or permanent and are sometimes decorative. + Bollard, + /// Chassis cab truck with an enclosed cube shaped cargo area. It should be noted that the cargo area is rigidly attached to the cab, and they do not articulate. + BoxTruck, + /// Standard city buses designed to carry a large number of people. + Bus, + /// Construction Barrel is a movable traffic barrel that is used to alert drivers to a hazard. These will typically be orange and white striped and may or may not have a blinking light attached to the top. + ConstructionBarrel, + /// Movable traffic cone that is used to alert drivers to a hazard. These will typically be orange and white striped and may or may not have a blinking light attached to the top. + ConstructionCone, + /// Any member of the canine family. + Dog, + /// Large motorized vehicles (four wheels or more) which do not fit into any more specific subclass. Examples include extended passenger vans, fire trucks, RVs, etc. + LargeVehicle, + /// Trailer carrying a large, mounted, electronic sign to display messages. Often found around construction sites or large events. + MessageBoardTrailer, + /// Movable sign designating an area where pedestrians may cross the road. + MobilePedestrianCrossingSign, + /// Motorized vehicle with two wheels where the rider straddles the engine. These are capable of high speeds similar to a car. + Motorcycle, + /// Person actively riding a motorcycle or a moped, including passengers. + Motorcyclist, + /// Person with authority specifically responsible for stopping and directing vehicles through traffic. + OfficialSignaler, + /// Person that is not driving or riding in/on a vehicle. They can be walking, standing, sitting, prone, etc. + Pedestrian, + /// Any vehicle that relies on rails to move. This applies to trains, trolleys, train engines, train freight cars, train tanker cars, subways, etc. + RailedVehicle, + /// Any conventionally sized passenger vehicle used for the transportation of people and cargo. This includes Cars, vans, pickup trucks, SUVs, etc. + RegularVehicle, + /// Bus that primarily holds school children (typically yellow) and can control the flow of traffic via the use of an articulating stop sign and loading/unloading flasher lights. + SchoolBus, + /// Official road signs placed by the Department of Transportation (DOT signs) which are of interest to us. This includes yield signs, speed limit signs, directional control signs, construction signs, and other signs that provide required traffic control information. Note that Stop Sign is captured separately and informative signs such as street signs, parking signs, bus stop signs, etc. are not included in this class. + Sign, + /// Red octagonal traffic sign displaying the word STOP used to notify drivers that they must come to a complete stop and make sure no other road users are coming before proceeding. + StopSign, + /// Push-cart with wheels meant to hold a baby or toddler. + Stroller, + /// Mounted, portable traffic light unit commonly used in construction zones or for other temporary detours. + TrafficLightTrailer, + /// Vehicles that are clearly defined as a truck but does not fit into the subclasses of Box Truck or Truck Cab. Examples include common delivery vehicles (UPS, FedEx), mail trucks, garbage trucks, utility trucks, ambulances, dump trucks, etc. + Truck, + /// Heavy truck commonly known as “Semi cab”, “Tractor”, or “Lorry”. This refers to only the front of part of an articulated tractor trailer. + TruckCab, + /// Non-motorized, wheeled vehicle towed behind a motorized vehicle. + VehicularTrailer, + /// Chair fitted with wheels for use as a means of transport by a person who is unable to walk as a result of illness, injury, or disability. This includes both motorized and non-motorized wheelchairs as well as low-speed seated scooters not intended for use on the roadway. + Wheelchair, + /// Objects involved in the transportation of a person and do not fit a more specific class. Examples range from skateboards, non-motorized scooters, segways, to golf-carts. + WheeledDevice, + /// Person actively riding or being carried by a wheeled device. + WheeledRider, +} + +/// Argoverse 2 camera names. +#[derive(Clone, Copy, Debug, Display, EnumIter, EnumString, PartialEq)] +#[strum(serialize_all = "snake_case")] +pub enum CameraNames { + /// Ring rear left RGB camera. + RingRearLeft, + /// Ring side left RGB camera. + RingSideLeft, + /// Ring front left RGB camera. + RingFrontLeft, + /// Ring front center RGB camera. + RingFrontCenter, + /// Ring front right RGB camera. + RingFrontRight, + /// Ring side right RGB camera. + RingSideRight, + /// Ring rear right RGB camera. + RingRearRight, +} diff --git a/rust/src/data_loader.rs b/rust/src/data_loader.rs new file mode 100644 index 00000000..8e842d9a --- /dev/null +++ b/rust/src/data_loader.rs @@ -0,0 +1,642 @@ +//! # data_loaders +//! +//! Data-loader for loading the sensor dataset. + +use constants::{ANNOTATION_COLUMNS, POSE_COLUMNS}; + +use image::ImageBuffer; +use image::Rgba; +use io::{read_accumulate_lidar, read_timestamped_feather}; +use itertools::Itertools; +use ndarray::Ix3; +use nshare::ToNdarray3; +use numpy::IntoPyArray; +use numpy::PyArray; +use pyo3::prelude::*; +use pyo3_polars::PyDataFrame; +use rayon::prelude::IntoParallelRefIterator; +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; +use strum::IntoEnumIterator; + +use glob::glob; +use polars::prelude::*; + +use crate::io::read_image_rgba8; +use crate::{ + constants::{self, CameraNames}, + geometry::camera::pinhole_camera::PinholeCamera, + io::{self}, + structures::timestamped_image::TimeStampedImage, +}; +use rayon::iter::IndexedParallelIterator; +use rayon::iter::ParallelIterator; + +const MIN_NUM_LIDAR_PTS: u64 = 1; +const MAX_CAM_LIDAR_TOL_NS: f32 = 50000000.; + +/// Data associated with a single lidar sweep. +#[pyclass] +#[derive(Clone, Debug)] +pub struct Sweep { + /// Ego-vehicle city pose. + #[pyo3(get, set)] + pub city_pose: PyDataFrame, + /// Point cloud associated with the sweep. + #[pyo3(get, set)] + pub lidar: PyDataFrame, + /// Log id and nanosecond timestamp (unique identifier). + #[pyo3(get, set)] + pub sweep_uuid: (String, u64), + /// Cuboids associated with the sweep. + #[pyo3(get, set)] + pub cuboids: Option, +} + +/// Encapsulates sensor data associated with a single sweep. +#[pymethods] +impl Sweep { + /// Initialize a sweep object. + #[new] + pub fn new( + annotations: PyDataFrame, + city_pose: PyDataFrame, + lidar: PyDataFrame, + sweep_uuid: (String, u64), + ) -> Sweep { + Sweep { + city_pose, + lidar, + sweep_uuid, + cuboids: Some(annotations), + } + } +} + +/// Sensor data-loader for `av2`. +#[pyclass(module = "av2._r")] +pub struct DataLoader { + /// Root dataset directory. + #[pyo3(get, set)] + pub root_dir: PathBuf, + /// Dataset name (e.g., `av2`). + #[pyo3(get, set)] + pub dataset_name: String, + /// Root dataset type (e.g., `sensor`). + #[pyo3(get, set)] + pub dataset_type: String, + /// Root dataset split name (e.g., `train`). + #[pyo3(get, set)] + pub split_name: String, + /// Number of accumulated lidar sweeps. + #[pyo3(get, set)] + pub num_accumulated_sweeps: usize, + /// Boolean flag to enable memory-mapped data-frame loading. + #[pyo3(get, set)] + pub memory_mapped: bool, + /// Data-frame consisting of `log_id`, `timestamp_ns`, and `city_name`. + #[pyo3(get, set)] + pub file_index: PyDataFrame, + /// Current index of the data-loader. + #[pyo3(get, set)] + pub current_index: usize, +} + +/// Pythod bound methods are found here. +#[pymethods] +impl DataLoader { + /// Initialize the data-loader and build the file index. + #[new] + pub fn new( + root_dir: &str, + dataset_name: &str, + dataset_type: &str, + split_name: &str, + num_accumulated_sweeps: usize, + memory_mapped: bool, + ) -> DataLoader { + let root_dir = PathBuf::from_str(root_dir).unwrap(); + let file_index = PyDataFrame(build_file_index( + root_dir.as_path(), + dataset_name, + dataset_type, + split_name, + )); + + let dataset_name = dataset_name.to_string(); + let dataset_type = dataset_type.to_string(); + let split_name = split_name.to_string(); + let current_index = 0; + DataLoader { + root_dir, + dataset_name, + dataset_type, + split_name, + num_accumulated_sweeps, + memory_mapped, + file_index, + current_index, + } + } + + fn read_city_pose_py(&self, log_id: &str, timestamp_ns: u64) -> PyDataFrame { + PyDataFrame(self.read_city_pose(log_id, timestamp_ns)) + } + + fn read_lidar_py(&self, log_id: &str, timestamp_ns: u64, index: usize) -> PyDataFrame { + PyDataFrame(self.read_lidar(log_id, timestamp_ns, index)) + } + + fn read_annotations_py(&self, log_id: &str, timestamp_ns: u64) -> PyDataFrame { + PyDataFrame(self.read_annotations(log_id, timestamp_ns)) + } + + /// Get the sweep at `index`. + pub fn get(&self, index: usize) -> Sweep { + let row = self.file_index.0.get_row(index).unwrap().0; + let (log_id, timestamp_ns) = ( + row.first().unwrap().get_str().unwrap(), + row.get(1).unwrap().try_extract::().unwrap(), + ); + + // Annotations aren't available for the test set. + let cuboids = match self.split_name.as_str() { + "test" => None, + _ => Some(self.read_annotations_py(log_id, timestamp_ns)), + }; + + let city_pose = self.read_city_pose_py(log_id, timestamp_ns); + let lidar = self.read_lidar_py(log_id, timestamp_ns, index); + let sweep_uuid = (log_id.to_string(), timestamp_ns); + + Sweep { + city_pose, + lidar, + sweep_uuid, + cuboids, + } + } + + /// Get all synchronized images at the sweep index. + #[pyo3(name = "get_synchronized_images")] + pub fn py_get_synchronized_images<'py>( + &self, + py: Python<'py>, + index: usize, + ) -> Vec<&'py PyArray> { + let images = self.get_synchronized_images(index); + images + .into_iter() + .map(|x| x.image.into_ndarray3().into_pyarray(py)) + .collect_vec() + } + + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + slf.next() + } + + fn __len__(slf: PyRef<'_, Self>) -> usize { + slf.file_index.0.shape().0 + } +} + +/// Rust methods. +impl DataLoader { + /// Return the data loader length. + #[must_use] + #[inline] + pub fn len(&self) -> usize { + self.file_index.0.shape().0 + } + + /// Returns `true` if `self` has a length of zero bytes. + #[must_use] + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Log split directory. + /// E.g., ~/data/datasets/av2/sensor/. + pub fn split_dir(&self) -> PathBuf { + self.root_dir + .join(&self.dataset_name) + .join(&self.dataset_type) + .join(&self.split_name) + } + + /// Log split directory. + /// E.g., /. + pub fn log_dir(&self, log_id: &str) -> PathBuf { + self.split_dir().join(log_id) + } + + /// Camera path for the specified `camera_name` in `log_id` captured at `timestamp_ns`. + /// `//.jpg`. + pub fn camera_path(&self, log_id: &str, camera_name: &str, timestamp_ns: u64) -> PathBuf { + let file_name = format!("{timestamp_ns}.jpg"); + let sensor_path = [ + self.log_dir(log_id), + "sensors".to_string().into(), + "cameras".to_string().into(), + camera_name.to_string().into(), + file_name.into(), + ] + .iter() + .collect(); + sensor_path + } + + /// Annotations path associated with `log_id`. + /// This includes annotations for _every_ sweep in the log. + /// You will need to filter these at `timestamp_ns`. + /// E.g., `/annotations.feather`. + pub fn annotations_path(&self, log_id: &str) -> PathBuf { + self.log_dir(log_id).join("annotations.feather") + } + + /// City path associated with `log_id`. + /// This includes city egovehicle pose for _every_ sweep in the log. + /// You will need to filter these at `timestamp_ns`. + /// E.g., `/city_SE3_egovehicle.feather`. + pub fn city_pose_path(&self, log_id: &str) -> PathBuf { + self.log_dir(log_id).join("city_SE3_egovehicle.feather") + } + + /// Read the annotations occuring in `log_id` between `[timestamp_ns + timestamp_ns + ~0.1 s]`. + pub fn read_annotations(&self, log_id: &str, timestamp_ns: u64) -> DataFrame { + read_timestamped_feather( + &self.annotations_path(log_id), + &ANNOTATION_COLUMNS.to_vec(), + ×tamp_ns, + self.memory_mapped, + ) + .filter(col("num_interior_pts").gt_eq(MIN_NUM_LIDAR_PTS)) + .collect() + .unwrap() + } + + /// Read city egovehicle pose occuring at `timestamp_ns`. + pub fn read_city_pose(&self, log_id: &str, timestamp_ns: u64) -> DataFrame { + read_timestamped_feather( + &self.city_pose_path(log_id), + &POSE_COLUMNS.to_vec(), + ×tamp_ns, + self.memory_mapped, + ) + .collect() + .unwrap() + } + + /// Read the lidar occuring in `log_id` between `[timestamp_ns + timestamp_ns + ~0.1 s]`. + pub fn read_lidar(&self, log_id: &str, timestamp_ns: u64, index: usize) -> DataFrame { + read_accumulate_lidar( + self.log_dir(log_id), + &self.file_index.0, + log_id, + timestamp_ns, + index, + self.num_accumulated_sweeps, + self.memory_mapped, + ) + .collect() + .unwrap() + } + + /// Get the sweep at `index`. + pub fn get_synchronized_images(&self, index: usize) -> Vec { + let row = self.file_index.0.get_row(index).unwrap().0; + let (log_id, _) = ( + row.first().unwrap().get_str().unwrap(), + row.get(1).unwrap().try_extract::().unwrap(), + ); + + let camera_names = CameraNames::iter().collect_vec(); + let images = camera_names + .par_iter() + .enumerate() + .map(|(i, camera_name)| { + let maybe_timestamp_ns_camera = row.get(i + 3).unwrap().try_extract::(); + match maybe_timestamp_ns_camera { + Ok(maybe_timestamp_ns_camera) => { + let timestamp_ns_camera = maybe_timestamp_ns_camera; + let camera_path = self.camera_path( + log_id, + camera_name.to_string().as_str(), + timestamp_ns_camera, + ); + let image = TimeStampedImage { + image: read_image_rgba8(&camera_path), + camera_model: PinholeCamera::from_feather( + &self.log_dir(log_id), + camera_name.to_string().as_str(), + ), + timestamp_ns: timestamp_ns_camera as usize, + }; + image + } + _ => { + let camera_name = camera_name.to_string(); + let (width, height) = if camera_name == "ring_front_center" { + (1550, 2048) + } else { + (2048, 1550) + }; + + TimeStampedImage { + image: ImageBuffer::, Vec>::from_raw( + width, + height, + vec![0; 4 * height as usize * width as usize], + ) + .unwrap(), + camera_model: PinholeCamera::from_feather( + &self.log_dir(log_id), + camera_name.as_str(), + ), + timestamp_ns: usize::MAX, + } + } + } + }) + .collect(); + images + } +} + +impl Iterator for DataLoader { + type Item = Sweep; + + fn next(&mut self) -> Option { + let idx = self.current_index; + let sweep_data = self.get(idx); + self.current_index += 1; + + Some(sweep_data) + } +} + +fn build_file_index( + root_dir: &Path, + dataset_name: &str, + dataset_type: &str, + split_name: &str, +) -> DataFrame { + let split_dir = root_dir.join(format!("{dataset_name}/{dataset_type}/{split_name}")); + let mut reference_frame = build_lidar_metadata(split_dir.clone(), "lidar"); + + reference_frame = reference_frame + .clone() + .lazy() + .sort_by_exprs( + &[cols(["log_id", "timestamp_ns_lidar"])], + vec![false], + false, + true, + ) + .collect() + .unwrap(); + + for camera_name in CameraNames::iter().map(|x| x.to_string()) { + let frame = build_camera_metadata(split_dir.clone(), &camera_name); + frame + .clone() + .lazy() + .sort_by_exprs( + &[cols([ + "log_id", + format!("timestamp_ns_{camera_name}").as_str(), + ])], + vec![false], + false, + true, + ) + .collect() + .unwrap(); + + reference_frame = reference_frame + .join_asof_by( + &frame, + "timestamp_ns_lidar", + format!("timestamp_ns_{}", camera_name).as_str(), + ["log_id"], + ["log_id"], + AsofStrategy::Nearest, + Some(AnyValue::Float32(MAX_CAM_LIDAR_TOL_NS)), + ) + .unwrap(); + + reference_frame = reference_frame.drop_many(&["city_name_right"]); + } + reference_frame + .rename("timestamp_ns_lidar", "timestamp_ns") + .unwrap(); + reference_frame +} + +fn build_lidar_metadata(split_dir: PathBuf, sensor_name: &str) -> DataFrame { + let pattern = split_dir.join(format!("*/sensors/{sensor_name}/*.feather")); + let files = glob(pattern.to_str().unwrap()) + .expect("Failed to read glob pattern.") + .filter_map(|path| match path { + Ok(x) => Some(x), + _ => None, + }) + .collect_vec(); + + let log_id: Vec<_> = files + .iter() + .map(|x| { + x.ancestors() + .nth(3) + .unwrap() + .file_stem() + .unwrap() + .to_str() + .unwrap() + }) + .collect(); + + let timestamp_ns: Vec<_> = files + .clone() + .iter() + .map(|x| { + x.file_stem() + .unwrap() + .to_str() + .unwrap() + .parse::() + .unwrap() + }) + .collect(); + df!( + "log_id" => Series::new("log_id", log_id), + format!("timestamp_ns_{}", sensor_name).as_str() => Series::new("timestamp_ns", timestamp_ns.clone()), + "city_name" => Series::new("city_name", vec!["DEFAULT"; timestamp_ns.len()]) + ) + .unwrap() +} + +fn build_camera_metadata(split_dir: PathBuf, sensor_name: &str) -> DataFrame { + let pattern = split_dir.join(format!("*/sensors/cameras/{sensor_name}/*.jpg")); + let files = glob(pattern.to_str().unwrap()) + .expect("Failed to read glob pattern.") + .filter_map(|path| match path { + Ok(x) => Some(x), + _ => None, + }) + .collect_vec(); + + let log_id: Vec<_> = files + .iter() + .map(|x| { + x.ancestors() + .nth(4) + .unwrap() + .file_stem() + .unwrap() + .to_str() + .unwrap() + }) + .collect(); + + let timestamp_ns: Vec<_> = files + .clone() + .iter() + .map(|x| { + x.file_stem() + .unwrap() + .to_str() + .unwrap() + .parse::() + .unwrap() + }) + .collect(); + df!( + "log_id" => Series::new("log_id", log_id), + format!("timestamp_ns_{}", sensor_name).as_str() => Series::new("timestamp_ns", timestamp_ns.clone()), + "city_name" => Series::new("city_name", vec!["DEFAULT"; timestamp_ns.len()]) + ) + .unwrap() +} + +// fn _fast_build( +// root_dir: &Path, +// dataset_name: &str, +// dataset_type: &str, +// split_name: &str, +// ) -> DataFrame { +// let split_dir = root_dir.join(format!("{dataset_name}/{dataset_type}/{split_name}")); +// let synchronization_list = std::fs::read_dir(split_dir) +// .unwrap() +// .filter_map(|log_dir| match log_dir { +// Ok(log_dir) => { +// let mut reference_frame = extract_lidar_metadata(&log_dir.path()); +// for camera_name in CameraNames::iter().map(|x| x.to_string()) { +// let frame = extract_camera_metadata(&log_dir.path(), camera_name.as_str()); +// frame +// .clone() +// .lazy() +// .sort_by_exprs( +// &[cols([ +// "log_id", +// format!("timestamp_ns_{camera_name}").as_str(), +// ])], +// vec![false], +// false, +// ) +// .collect() +// .unwrap(); + +// reference_frame = reference_frame +// .join_asof_by( +// &frame, +// "timestamp_ns_lidar", +// format!("timestamp_ns_{}", camera_name).as_str(), +// ["log_id"], +// ["log_id"], +// AsofStrategy::Forward, +// Some(AnyValue::Float32(MAX_CAM_LIDAR_TOL_NS)), +// ) +// .unwrap(); + +// reference_frame = reference_frame.drop_many(&["city_name_right"]); +// } +// reference_frame +// .rename("timestamp_ns_lidar", "timestamp_ns") +// .unwrap(); +// Some(reference_frame.lazy()) +// } +// _ => None, +// }) +// .collect_vec(); +// concat(synchronization_list, true, true) +// .unwrap() +// .collect() +// .unwrap() +// } + +// fn extract_lidar_metadata(log_dir: &Path) -> DataFrame { +// let lidar_pattern = log_dir.join("sensors/lidar/*.feather"); +// let lidar_files = glob(lidar_pattern.to_str().unwrap()) +// .expect("Failed to read glob pattern.") +// .filter_map(|path| match path { +// Ok(x) => Some(x), +// _ => None, +// }) +// .collect_vec(); +// let log_id = log_dir.file_stem().unwrap().to_str().unwrap(); +// let timestamp_ns: Vec<_> = lidar_files +// .iter() +// .map(|x| { +// x.file_stem() +// .unwrap() +// .to_str() +// .unwrap() +// .parse::() +// .unwrap() +// }) +// .collect(); +// df!( +// "log_id" => Series::new("log_id", vec![log_id; timestamp_ns.len()]), +// "timestamp_ns_lidar" => Series::new("timestamp_ns", timestamp_ns.clone()), +// "city_name" => Series::new("city_name", vec!["DEFAULT"; timestamp_ns.len()]) +// ) +// .unwrap() +// } + +// fn extract_camera_metadata(log_dir: &Path, camera_name: &str) -> DataFrame { +// let camera_pattern = log_dir.join(format!("sensors/cameras/{camera_name}/*.jpg")); +// let camera_files = glob(camera_pattern.to_str().unwrap()) +// .expect("Failed to read glob pattern.") +// .filter_map(|path| match path { +// Ok(x) => Some(x), +// _ => None, +// }) +// .collect_vec(); + +// let log_id = log_dir.file_stem().unwrap().to_str().unwrap(); +// let timestamp_ns: Vec<_> = camera_files +// .iter() +// .map(|x| { +// x.file_stem() +// .unwrap() +// .to_str() +// .unwrap() +// .parse::() +// .unwrap() +// }) +// .collect(); +// df!( +// "log_id" => Series::new("log_id", vec![log_id; timestamp_ns.len()]), +// format!("timestamp_ns_{}", camera_name).as_str() => Series::new("timestamp_ns", timestamp_ns.clone()), +// "city_name" => Series::new("city_name", vec!["DEFAULT"; timestamp_ns.len()]) +// ) +// .unwrap() +// } diff --git a/rust/src/geometry/augmentations.rs b/rust/src/geometry/augmentations.rs new file mode 100644 index 00000000..3fc7b4c7 --- /dev/null +++ b/rust/src/geometry/augmentations.rs @@ -0,0 +1,324 @@ +//! # augmentations +//! +//! Geometric augmentations. + +use std::f32::consts::PI; + +use crate::{ + geometry::so3::_mat3_to_quat, + io::ndarray_from_frame, + share::{data_frame_to_ndarray_f32, ndarray_to_expr_vec}, +}; +use itertools::Itertools; +use ndarray::{azip, concatenate, par_azip, s, Array, Axis, Ix1, Ix2}; +use polars::{ + lazy::dsl::{col, cols, GetOutput}, + prelude::{DataFrame, DataType, IntoLazy}, + series::Series, +}; +use rand_distr::{Bernoulli, Distribution, Uniform}; + +use super::{ + polytope::{compute_interior_points_mask, cuboids_to_polygons}, + so3::{ + _quat_to_mat3, reflect_orientation_x, reflect_orientation_y, reflect_translation_x, + reflect_translation_y, + }, +}; + +/// Sample a scene reflection. +/// This reflects both a point cloud and cuboids across the x-axis. +pub fn sample_scene_reflection_x( + lidar: DataFrame, + cuboids: DataFrame, + p: f64, +) -> (DataFrame, DataFrame) { + let distribution = Bernoulli::new(p).unwrap(); + let is_augmented = distribution.sample(&mut rand::thread_rng()); + if is_augmented { + let augmented_lidar = lidar + .lazy() + .with_column(col("y").map( + move |x| { + Ok(Some( + x.f32() + .unwrap() + .into_no_null_iter() + .map(|y| -y) + .collect::(), + )) + }, + GetOutput::from_type(DataType::Float32), + )) + .collect() + .unwrap(); + + let translation_column_names = vec!["tx_m", "ty_m", "tz_m"]; + let txyz_m = data_frame_to_ndarray_f32(cuboids.clone(), translation_column_names.clone()); + let augmentation_translation = reflect_translation_x(&txyz_m.view()); + + let orientation_column_names = vec!["qw", "qx", "qy", "qz"]; + let quat_wxyz = + data_frame_to_ndarray_f32(cuboids.clone(), orientation_column_names.clone()); + let augmented_orientation = reflect_orientation_x(&quat_wxyz.view()); + let augmented_poses = + concatenate![Axis(1), augmentation_translation, augmented_orientation]; + + let column_names = translation_column_names + .into_iter() + .chain(orientation_column_names) + .collect_vec(); + let series_vec = ndarray_to_expr_vec(augmented_poses, column_names); + let augmented_cuboids = cuboids.lazy().with_columns(series_vec).collect().unwrap(); + (augmented_lidar, augmented_cuboids) + } else { + (lidar, cuboids) + } +} + +/// Sample a scene reflection. +/// This reflects both a point cloud and cuboids across the y-axis. +pub fn sample_scene_reflection_y( + lidar: DataFrame, + cuboids: DataFrame, + p: f64, +) -> (DataFrame, DataFrame) { + let distribution: Bernoulli = Bernoulli::new(p).unwrap(); + let is_augmented = distribution.sample(&mut rand::thread_rng()); + if is_augmented { + let augmented_lidar = lidar + .lazy() + .with_column(col("x").map( + move |x| { + Ok(Some( + x.f32() + .unwrap() + .into_no_null_iter() + .map(|x| -x) + .collect::(), + )) + }, + GetOutput::from_type(DataType::Float32), + )) + .collect() + .unwrap(); + + let translation_column_names = vec!["tx_m", "ty_m", "tz_m"]; + let txyz_m = data_frame_to_ndarray_f32(cuboids.clone(), translation_column_names.clone()); + let augmentation_translation = reflect_translation_y(&txyz_m.view()); + + let orientation_column_names = vec!["qw", "qx", "qy", "qz"]; + let quat_wxyz = + data_frame_to_ndarray_f32(cuboids.clone(), orientation_column_names.clone()); + let augmented_orientation = reflect_orientation_y(&quat_wxyz.view()); + let augmented_poses = + concatenate![Axis(1), augmentation_translation, augmented_orientation]; + + let column_names = translation_column_names + .into_iter() + .chain(orientation_column_names) + .collect_vec(); + let series_vec = ndarray_to_expr_vec(augmented_poses, column_names); + let augmented_cuboids = cuboids.lazy().with_columns(series_vec).collect().unwrap(); + (augmented_lidar, augmented_cuboids) + } else { + (lidar, cuboids) + } +} + +/// Sample a scene global scale. +/// This scales the lidar coordinates (x,y,z) and the cuboid centers (tx_m,ty_m,tz_m). +pub fn sample_scene_global_scale( + lidar: DataFrame, + cuboids: DataFrame, + low_inclusive: f64, + upper_inclusive: f64, +) -> (DataFrame, DataFrame) { + let distribution = Uniform::new_inclusive(low_inclusive, upper_inclusive); + let scale_factor = distribution.sample(&mut rand::thread_rng()) as f32; + let augmented_lidar = lidar + .lazy() + .with_column(col("x").map( + move |x| { + Ok(Some( + x.f32() + .unwrap() + .into_no_null_iter() + .map(|x| scale_factor * x) + .collect::(), + )) + }, + GetOutput::from_type(DataType::Float32), + )) + .with_column(col("y").map( + move |y| { + Ok(Some( + y.f32() + .unwrap() + .into_no_null_iter() + .map(|y| scale_factor * y) + .collect::(), + )) + }, + GetOutput::from_type(DataType::Float32), + )) + .with_column(col("z").map( + move |z| { + Ok(Some( + z.f32() + .unwrap() + .into_no_null_iter() + .map(|z| scale_factor * z) + .collect::(), + )) + }, + GetOutput::from_type(DataType::Float32), + )) + .collect() + .unwrap(); + + let augmented_cuboids = cuboids + .lazy() + .with_column(col("tx_m").map( + move |x| { + Ok(Some( + x.f32() + .unwrap() + .into_no_null_iter() + .map(|x| scale_factor * x) + .collect::(), + )) + }, + GetOutput::from_type(DataType::Float32), + )) + .with_column(col("ty_m").map( + move |y| { + Ok(Some( + y.f32() + .unwrap() + .into_no_null_iter() + .map(|y| scale_factor * y) + .collect::(), + )) + }, + GetOutput::from_type(DataType::Float32), + )) + .with_column(col("tz_m").map( + move |z| { + Ok(Some( + z.f32() + .unwrap() + .into_no_null_iter() + .map(|z| scale_factor * z) + .collect::(), + )) + }, + GetOutput::from_type(DataType::Float32), + )) + .collect() + .unwrap(); + (augmented_lidar, augmented_cuboids) +} + +/// Sample a scene global rotation. +/// This rotates the lidar coordinates (x,y,z) and the cuboid centers (tx_m,ty_m,tz_m). +pub fn sample_scene_global_rotation( + lidar: DataFrame, + cuboids: DataFrame, + low_inclusive: f64, + upper_inclusive: f64, +) -> (DataFrame, DataFrame) { + let distribution = Uniform::new_inclusive(low_inclusive, upper_inclusive); + let theta = distribution.sample(&mut rand::thread_rng()) as f32; + let rotation = Array::::from_vec(vec![ + f32::cos(2.0 * PI * theta), + f32::sin(2.0 * PI * theta), + 0.0, + -f32::sin(2.0 * PI * theta), + f32::cos(2.0 * PI * theta), + 0.0, + 0.0, + 0.0, + 1.0, + ]) + .into_shape((3, 3)) + .unwrap(); + + let column_names = ["x", "y", "z"]; + let lidar_ndarray = ndarray_from_frame(&lidar, cols(column_names)); + let augmented_lidar_ndarray = lidar_ndarray.dot(&rotation); + let series_vec = ndarray_to_expr_vec(augmented_lidar_ndarray, column_names.to_vec()); + let augmented_lidar = lidar.lazy().with_columns(series_vec).collect().unwrap(); + + let cuboid_column_names = ["tx_m", "ty_m", "tz_m", "qw", "qx", "qy", "qz"]; + let cuboids_ndarray = ndarray_from_frame(&cuboids, cols(cuboid_column_names)); + + let num_cuboids = cuboids_ndarray.shape()[0]; + let mut augmented_cuboids = Array::::zeros((num_cuboids, 7)); + par_azip!((mut ac in augmented_cuboids.outer_iter_mut(), c in cuboids_ndarray.outer_iter()) { + let augmented_translation = c.slice(s![..3]).dot(&rotation); + let augmented_mat3 = _quat_to_mat3(&c.slice(s![3..7])).dot(&rotation.t()); + let augmented_rotation = _mat3_to_quat(&augmented_mat3.view()); + + ac.slice_mut(s![..3]).assign(&augmented_translation); + ac.slice_mut(s![3..7]).assign(&augmented_rotation); + }); + + let series_vec = ndarray_to_expr_vec(augmented_cuboids, cuboid_column_names.to_vec()); + let augmented_cuboids = cuboids.lazy().with_columns(series_vec).collect().unwrap(); + (augmented_lidar, augmented_cuboids) +} + +/// Sample a scene with random object scaling. +pub fn sample_random_object_scale( + lidar: DataFrame, + cuboids: DataFrame, + low_inclusive: f64, + high_inclusive: f64, +) -> (DataFrame, DataFrame) { + let cuboid_column_names = [ + "tx_m", "ty_m", "tz_m", "length_m", "width_m", "height_m", "qw", "qx", "qy", "qz", + ]; + let mut lidar_ndarray = ndarray_from_frame(&lidar, cols(["x", "y", "z"])); + let mut cuboids_ndarray = ndarray_from_frame(&cuboids, cols(cuboid_column_names)); + let cuboid_vertices = cuboids_to_polygons(&cuboids_ndarray.view()); + let interior_points_mask = + compute_interior_points_mask(&lidar_ndarray.view(), &cuboid_vertices.view()); + + let distribution = Uniform::new_inclusive(low_inclusive, high_inclusive); + + azip!((mut c in cuboids_ndarray.outer_iter_mut(), m in interior_points_mask.outer_iter()) { + let scale_factor = distribution.sample(&mut rand::thread_rng()) as f32; + let indices = m + .iter() + .enumerate() + .filter_map(|(i, x)| match *x { + true => Some(i), + _ => None, + }) + .collect_vec(); + let interior_points = lidar_ndarray.select(Axis(0), &indices); + + // TODO: Handle with iterators. + for (i, j) in indices.into_iter().enumerate() { + let scaled_lidar_ndarray_object = (&interior_points.slice(s![i, ..]) + - &c.slice(s![..3])) + * scale_factor; + let scaled_lidar_ndarray_egovehicle = + &scaled_lidar_ndarray_object + &c.slice(s![..3]); + + lidar_ndarray + .slice_mut(s![j, ..]) + .assign(&scaled_lidar_ndarray_egovehicle); + } + c.slice_mut(s![3..6]).mapv_inplace(|x| x * scale_factor); + }); + + let lidar_column_names = vec!["x", "y", "z"]; + let series_vec = ndarray_to_expr_vec(lidar_ndarray, lidar_column_names); + let augmented_lidar = lidar.lazy().with_columns(series_vec).collect().unwrap(); + + let series_vec = ndarray_to_expr_vec(cuboids_ndarray, cuboid_column_names.to_vec()); + let augmented_cuboids = cuboids.lazy().with_columns(series_vec).collect().unwrap(); + (augmented_lidar, augmented_cuboids) +} diff --git a/rust/src/geometry/camera/mod.rs b/rust/src/geometry/camera/mod.rs new file mode 100644 index 00000000..8715119d --- /dev/null +++ b/rust/src/geometry/camera/mod.rs @@ -0,0 +1,2 @@ +/// Pinhole camera model. +pub mod pinhole_camera; diff --git a/rust/src/geometry/camera/pinhole_camera.rs b/rust/src/geometry/camera/pinhole_camera.rs new file mode 100644 index 00000000..b47b3ed8 --- /dev/null +++ b/rust/src/geometry/camera/pinhole_camera.rs @@ -0,0 +1,227 @@ +use std::{ops::DivAssign, path::Path}; + +use ndarray::{par_azip, s, Array, ArrayView, Ix1, Ix2}; +use polars::{ + lazy::dsl::{col, lit}, + prelude::{DataFrame, IntoLazy}, +}; + +use crate::{ + geometry::se3::SE3, geometry::so3::_quat_to_mat3, geometry::utils::cart_to_hom, + io::read_feather_eager, +}; + +/// Pinhole camera intrinsics. +#[derive(Clone, Debug)] +pub struct Intrinsics { + /// Horizontal focal length in pixels. + pub fx_px: f32, + /// Vertical focal length in pixels. + pub fy_px: f32, + /// Horizontal focal center in pixels. + pub cx_px: f32, + /// Vertical focal center in pixels. + pub cy_px: f32, + /// Width of image in pixels. + pub width_px: usize, + /// Height of image in pixels. + pub height_px: usize, +} + +impl Intrinsics { + /// Construct a new `Intrinsics` instance. + pub fn new( + &self, + fx_px: f32, + fy_px: f32, + cx_px: f32, + cy_px: f32, + width_px: usize, + height_px: usize, + ) -> Self { + Self { + fx_px, + fy_px, + cx_px, + cy_px, + width_px, + height_px, + } + } + + /// Camera intrinsic matrix. + pub fn k(&self) -> Array { + let mut k = Array::::eye(3); + k[[0, 0]] = self.fx_px; + k[[1, 1]] = self.fy_px; + k[[0, 2]] = self.cx_px; + k[[1, 2]] = self.cy_px; + k + } +} + +/// Parameterizes a pinhole camera with zero skew. +#[derive(Clone, Debug)] +pub struct PinholeCamera { + /// Pose of camera in the egovehicle frame (inverse of extrinsics matrix). + pub ego_se3_cam: SE3, + /// `Intrinsics` object containing intrinsic parameters and image dimensions. + pub intrinsics: Intrinsics, + /// Associated camera name. + pub camera_name: String, +} + +impl PinholeCamera { + /// Return the width of the image in pixels. + pub fn width_px(&self) -> usize { + self.intrinsics.width_px + } + + /// Return the height of the image in pixels.""" + pub fn height_px(&self) -> usize { + self.intrinsics.height_px + } + + /// Return the camera extrinsics. + pub fn extrinsics(&self) -> Array { + self.ego_se3_cam.inverse().transform_matrix() + } + + /// Camera projection matrix. + pub fn p(&self) -> Array { + let mut p = Array::::zeros([3, 4]); + p.slice_mut(s![..3, ..3]).assign(&self.intrinsics.k()); + p + } + + /// Create a pinhole camera model from a feather file. + pub fn from_feather(log_dir: &Path, camera_name: &str) -> PinholeCamera { + let intrinsics_path = log_dir.join("calibration/intrinsics.feather"); + let intrinsics = read_feather_eager(&intrinsics_path, false) + .lazy() + .filter(col("sensor_name").eq(lit(camera_name))) + .collect() + .unwrap(); + + let intrinsics = Intrinsics { + fx_px: extract_f32_from_frame(&intrinsics, "fx_px"), + fy_px: extract_f32_from_frame(&intrinsics, "fy_px"), + cx_px: extract_f32_from_frame(&intrinsics, "cx_px"), + cy_px: extract_f32_from_frame(&intrinsics, "cy_px"), + width_px: extract_usize_from_frame(&intrinsics, "width_px"), + height_px: extract_usize_from_frame(&intrinsics, "height_px"), + }; + + let extrinsics_path = log_dir.join("calibration/egovehicle_SE3_sensor.feather"); + let extrinsics = read_feather_eager(&extrinsics_path, false) + .lazy() + .filter(col("sensor_name").eq(lit(camera_name))) + .collect() + .unwrap(); + + let qw = extract_f32_from_frame(&extrinsics, "qw"); + let qx = extract_f32_from_frame(&extrinsics, "qx"); + let qy = extract_f32_from_frame(&extrinsics, "qy"); + let qz = extract_f32_from_frame(&extrinsics, "qz"); + let tx_m = extract_f32_from_frame(&extrinsics, "tx_m"); + let ty_m = extract_f32_from_frame(&extrinsics, "ty_m"); + let tz_m = extract_f32_from_frame(&extrinsics, "tz_m"); + + let quat_wxyz = Array::::from_vec(vec![qw, qx, qy, qz]); + let rotation = _quat_to_mat3(&quat_wxyz.view()); + let translation = Array::::from_vec(vec![tx_m, ty_m, tz_m]); + let ego_se3_cam = SE3 { + rotation, + translation, + }; + + let camera_name_string = camera_name.to_string(); + Self { + ego_se3_cam, + intrinsics, + camera_name: camera_name_string, + } + } + + /// Cull 3D points to camera view frustum. + /// + /// Ref: https://en.wikipedia.org/wiki/Hidden-surface_determination#Viewing-frustum_culling + /// + /// Given a set of coordinates in the image plane and corresponding points + /// in the camera coordinate reference frame, determine those points + /// that have a valid projection into the image. 3D points with valid + /// projections have x coordinates in the range [0,width_px), y-coordinates + /// in the range [0,height_px), and a positive z-coordinate (lying in + /// front of the camera frustum). + pub fn cull_to_view_frustum( + self, + uv: &ArrayView, + points_camera: &ArrayView, + ) -> Array { + let num_points = uv.shape()[0]; + let mut is_within_frustum = Array::::from_vec(vec![false; num_points]) + .into_shape([num_points, 1]) + .unwrap(); + par_azip!((mut is_within_frustum_i in is_within_frustum.outer_iter_mut(), uv_row in uv.outer_iter(), point_cam in points_camera.outer_iter()) { + let is_within_frustum_x = (uv_row[0] >= 0.) && (uv_row[0] < self.width_px() as f32); + let is_within_frustum_y = (uv_row[1] >= 0.) && (uv_row[1] < self.height_px() as f32); + let is_within_frustum_z = point_cam[2] > 0.; + is_within_frustum_i[0] = is_within_frustum_x & is_within_frustum_y & is_within_frustum_z; + }); + is_within_frustum + } + + /// Project a collection of 3D points (provided in the egovehicle frame) to the image plane. + pub fn project_ego_to_image( + &self, + points_ego: Array, + ) -> (Array, Array, Array) { + // Convert cartesian to homogeneous coordinates. + let points_hom_ego = cart_to_hom(points_ego); + let points_hom_cam = points_hom_ego.dot(&self.extrinsics().t()); + + let mut uvz = points_hom_cam + .slice(s![.., ..3]) + .dot(&self.intrinsics.k().t()); + let z = uvz.slice(s![.., 2..3]).clone().to_owned(); + uvz.slice_mut(s![.., ..2]).div_assign(&z); + let is_valid_points = self + .clone() + .cull_to_view_frustum(&uvz.view(), &points_hom_cam.view()); + (uvz.to_owned(), points_hom_cam.to_owned(), is_valid_points) + } + + /// Project a collection of 3D points (provided in the egovehicle frame) to the image plane. + pub fn project_ego_to_image_motion_compensated( + &self, + points_ego: Array, + city_se3_ego_camera_t: SE3, + city_se3_ego_lidar_t: SE3, + ) -> (Array, Array, Array) { + // Convert cartesian to homogeneous coordinates. + let ego_cam_t_se3_ego_lidar_t = city_se3_ego_camera_t + .inverse() + .compose(&city_se3_ego_lidar_t); + + let points_ego = ego_cam_t_se3_ego_lidar_t.transform_from(&points_ego.view()); + self.project_ego_to_image(points_ego) + } +} + +fn extract_f32_from_frame(series: &DataFrame, column: &str) -> f32 { + series + .column(column) + .unwrap() + .get(0) + .unwrap() + .try_extract::() + .unwrap() +} + +fn extract_usize_from_frame(series: &DataFrame, column: &str) -> usize { + series[column] + .get(0) + .unwrap() + .try_extract::() + .unwrap() +} diff --git a/rust/src/geometry/mod.rs b/rust/src/geometry/mod.rs new file mode 100644 index 00000000..196e80d7 --- /dev/null +++ b/rust/src/geometry/mod.rs @@ -0,0 +1,16 @@ +//! # geometry +//! +//! Geometric operations for data processing. + +/// Geometric augmentations. +pub mod augmentations; +/// Camera models. +pub mod camera; +/// Geometric algorithms for polytopes. +pub mod polytope; +/// Special Euclidean Group 3. +pub mod se3; +/// Special Orthogonal Group 3. +pub mod so3; +/// Geometric utility functions. +pub mod utils; diff --git a/rust/src/geometry/polytope.rs b/rust/src/geometry/polytope.rs new file mode 100644 index 00000000..54d5df7a --- /dev/null +++ b/rust/src/geometry/polytope.rs @@ -0,0 +1,90 @@ +//! # polygons +//! +//! Geometric algorithms for polygon geometries. + +use ndarray::{concatenate, par_azip, s, Array, ArrayView, Axis, Ix1, Ix2, Ix3, Slice}; +use once_cell::sync::Lazy; + +use super::so3::_quat_to_mat3; + +// Safety: 24 elements (8 * 3 = 24) are defined. +static VERTS: Lazy> = Lazy::new(|| unsafe { + Array::::from_shape_vec_unchecked( + (8, 3), + vec![ + 1., 1., 1., 1., -1., 1., 1., -1., -1., 1., 1., -1., -1., 1., 1., -1., -1., 1., -1., + -1., -1., -1., 1., -1., + ], + ) +}); + +/// Compute a boolean mask indicating which points are interior to the cuboid geometry. +pub fn compute_interior_points_mask( + points: &ArrayView, + cuboid_vertices: &ArrayView, +) -> Array { + let num_points = points.shape()[0]; + let num_cuboids = cuboid_vertices.shape()[0]; + + let a = cuboid_vertices.slice_axis(Axis(1), Slice::from(6..7)); + let b = cuboid_vertices.slice_axis(Axis(1), Slice::from(3..4)); + let c = cuboid_vertices.slice_axis(Axis(1), Slice::from(1..2)); + let vertices = concatenate![Axis(1), a, b, c]; + + let reference_index = cuboid_vertices + .slice_axis(Axis(1), Slice::from(2..3)) + .to_owned(); + + let uvw = reference_index.clone() - vertices.clone(); + let reference_index = reference_index.into_shape((num_cuboids, 3)).unwrap(); + + let mut dot_uvw_reference = Array::::zeros((num_cuboids, 3)); + par_azip!((mut a in dot_uvw_reference.outer_iter_mut(), b in uvw.outer_iter(), c in reference_index.outer_iter()) a.assign(&b.dot(&c.t())) ); + + let mut dot_uvw_vertices = Array::::zeros((num_cuboids, 3)); + par_azip!((mut a in dot_uvw_vertices.outer_iter_mut(), b in uvw.outer_iter(), c in vertices.outer_iter()) a.assign(&b.dot(&c.t()).diag()) ); + + let dot_uvw_points = uvw + .into_shape((num_cuboids * 3, 3)) + .unwrap() + .as_standard_layout() + .dot(&points.t().as_standard_layout()) + .into_shape((num_cuboids, 3, num_points)) + .unwrap(); + + let shape = (num_cuboids, num_points); + let mut is_interior = + Array::<_, Ix2>::from_shape_vec(shape, vec![false; num_cuboids * num_points]).unwrap(); + par_azip!((mut a in is_interior.outer_iter_mut(), b in dot_uvw_reference.outer_iter(), c in dot_uvw_points.outer_iter(), d in dot_uvw_vertices.outer_iter()) { + + let c0 = c.slice(s![0, ..]).mapv(|x| ((b[0] <= x) & (x <= d[0])) | ((b[0] >= x) & (x >= d[0]))); + let c1 = c.slice(s![1, ..]).mapv(|x| ((b[1] <= x) & (x <= d[1])) | ((b[1] >= x) & (x >= d[1]))); + let c2 = c.slice(s![2, ..]).mapv(|x| ((b[2] <= x) & (x <= d[2])) | ((b[2] >= x) & (x >= d[2]))); + + let is_interior_i = &c0 & &c1 & &c2; + a.assign(&is_interior_i); + }); + + is_interior +} + +/// Convert (N,10) cuboids to polygons. +pub fn cuboids_to_polygons(cuboids: &ArrayView) -> Array { + let num_cuboids = cuboids.shape()[0]; + let mut polygons = Array::::zeros([num_cuboids, 8, 3]); + par_azip!((mut p in polygons.outer_iter_mut(), c in cuboids.outer_iter()) { + p.assign(&_cuboid_to_polygon(&c)) + }); + polygons +} + +/// Convert a single cuboid to a polygon. +fn _cuboid_to_polygon(cuboid: &ArrayView) -> Array { + let center_xyz = cuboid.slice(s![0..3]); + let dims_lwh = cuboid.slice(s![3..6]); + let quat_wxyz = cuboid.slice(s![6..10]); + let mat = _quat_to_mat3(&quat_wxyz); + let verts = &VERTS.clone() * &dims_lwh / 2.; + let verts = verts.dot(&mat.t()) + center_xyz; + verts.as_standard_layout().to_owned() +} diff --git a/rust/src/geometry/se3.rs b/rust/src/geometry/se3.rs new file mode 100644 index 00000000..9fc95d54 --- /dev/null +++ b/rust/src/geometry/se3.rs @@ -0,0 +1,60 @@ +//! # SE(3) +//! +//! Special Euclidean Group 3. + +use ndarray::ArrayView2; +use ndarray::{s, Array1, Array2}; + +/// Special Euclidean Group 3 (SE(3)). +/// Rigid transformation parameterized by a rotation and translation in $R^3$. +#[derive(Clone, Debug)] +pub struct SE3 { + /// (3,3) Orthonormal rotation matrix. + pub rotation: Array2, + /// (3,) Translation vector. + pub translation: Array1, +} + +impl SE3 { + /// Get the (4,4) homogeneous transformation matrix associated with the rigid transformation. + pub fn transform_matrix(&self) -> Array2 { + let mut transform_matrix = Array2::eye(4); + transform_matrix + .slice_mut(s![..3, ..3]) + .assign(&self.rotation); + transform_matrix + .slice_mut(s![..3, 3]) + .assign(&self.translation); + transform_matrix + } + + /// Transform the point cloud from its reference from to the SE(3) destination. + pub fn transform_from(&self, point_cloud: &ArrayView2) -> Array2 { + point_cloud.dot(&self.rotation.t()) + &self.translation + } + + /// Invert the SE(3) transformation. + pub fn inverse(&self) -> SE3 { + let rotation = self.rotation.t().as_standard_layout().to_owned(); + let translation = rotation.dot(&(-&self.translation)); + Self { + rotation, + translation, + } + } + + /// Compose (right multiply) an SE(3) with another SE(3). + pub fn compose(&self, right_se3: &SE3) -> SE3 { + let chained_transform_matrix = self.transform_matrix().dot(&right_se3.transform_matrix()); + SE3 { + rotation: chained_transform_matrix + .slice(s![..3, ..3]) + .as_standard_layout() + .to_owned(), + translation: chained_transform_matrix + .slice(s![..3, 3]) + .as_standard_layout() + .to_owned(), + } + } +} diff --git a/rust/src/geometry/so3.rs b/rust/src/geometry/so3.rs new file mode 100644 index 00000000..6d2d7718 --- /dev/null +++ b/rust/src/geometry/so3.rs @@ -0,0 +1,207 @@ +//! # SO(3) +//! +//! Special Orthogonal Group 3 (SO(3)). + +use std::f32::consts::PI; + +use ndarray::{par_azip, s, Array, Array2, ArrayView, Ix1, Ix2, Ix3}; +use rand_distr::{Distribution, StandardNormal}; + +/// Convert a quaternion in scalar-first format to a 3x3 rotation matrix. +/// Parallelized for batch processing. +pub fn quat_to_mat3(quat_wxyz: &ArrayView) -> Array { + let num_quats = quat_wxyz.shape()[0]; + let mut mat3 = Array::::zeros((num_quats, 3, 3)); + par_azip!((mut m in mat3.outer_iter_mut(), q in quat_wxyz.outer_iter()) { + m.assign(&_quat_to_mat3(&q)); + }); + mat3 +} + +/// Convert a quaternion in scalar-first format to a 3x3 rotation matrix. +pub fn _quat_to_mat3(quat_wxyz: &ArrayView) -> Array { + let w = quat_wxyz[0]; + let x = quat_wxyz[1]; + let y = quat_wxyz[2]; + let z = quat_wxyz[3]; + + let e_00 = 1. - 2. * y.powi(2) - 2. * z.powi(2); + let e_01: f32 = 2. * x * y - 2. * z * w; + let e_02: f32 = 2. * x * z + 2. * y * w; + + let e_10 = 2. * x * y + 2. * z * w; + let e_11 = 1. - 2. * x.powi(2) - 2. * z.powi(2); + let e_12 = 2. * y * z - 2. * x * w; + + let e_20 = 2. * x * z - 2. * y * w; + let e_21 = 2. * y * z + 2. * x * w; + let e_22 = 1. - 2. * x.powi(2) - 2. * y.powi(2); + + // Safety: We will always have nine elements. + unsafe { + Array2::from_shape_vec_unchecked( + [3, 3], + vec![e_00, e_01, e_02, e_10, e_11, e_12, e_20, e_21, e_22], + ) + } +} + +/// Convert a 3x3 rotation matrix to a scalar-first quaternion. +/// Parallelized for batch processing. +pub fn mat3_to_quat(mat3: &ArrayView) -> Array { + let num_transformations = mat3.shape()[0]; + let mut quat_wxyz = Array::::zeros((num_transformations, 4)); + par_azip!((mut q in quat_wxyz.outer_iter_mut(), m in mat3.outer_iter()) { + q.assign(&_mat3_to_quat(&m)) + }); + quat_wxyz +} + +/// Convert a 3x3 rotation matrix to a scalar-first quaternion. +pub fn _mat3_to_quat(mat3: &ArrayView) -> Array { + let trace = mat3[[0, 0]] + mat3[[1, 1]] + mat3[[2, 2]]; + let mut quat_wxyz = if trace > 0.0 { + let s = 0.5 / f32::sqrt(trace + 1.0); + let qw = 0.25 / s; + let qx = (mat3[[2, 1]] - mat3[[1, 2]]) * s; + let qy = (mat3[[0, 2]] - mat3[[2, 0]]) * s; + let qz = (mat3[[1, 0]] - mat3[[0, 1]]) * s; + Array::::from_vec(vec![qw, qx, qy, qz]) + } else if mat3[[0, 0]] > mat3[[1, 1]] && mat3[[0, 0]] > mat3[[2, 2]] { + let s = 2.0 * f32::sqrt(1.0 + mat3[[0, 0]] - mat3[[1, 1]] - mat3[[2, 2]]); + let qw = (mat3[[2, 1]] - mat3[[1, 2]]) / s; + let qx = 0.25 * s; + let qy = (mat3[[0, 1]] + mat3[[1, 0]]) / s; + let qz = (mat3[[0, 2]] + mat3[[2, 0]]) / s; + Array::::from_vec(vec![qw, qx, qy, qz]) + } else if mat3[[1, 1]] > mat3[[2, 2]] { + let s = 2.0 * f32::sqrt(1.0 + mat3[[1, 1]] - mat3[[0, 0]] - mat3[[2, 2]]); + let qw = (mat3[[0, 2]] - mat3[[2, 0]]) / s; + let qx = (mat3[[0, 1]] + mat3[[1, 0]]) / s; + let qy = 0.25 * s; + let qz = (mat3[[1, 2]] + mat3[[2, 1]]) / s; + Array::::from_vec(vec![qw, qx, qy, qz]) + } else { + let s = 2.0 * f32::sqrt(1.0 + mat3[[2, 2]] - mat3[[0, 0]] - mat3[[1, 1]]); + let qw = (mat3[[1, 0]] - mat3[[0, 1]]) / s; + let qx = (mat3[[0, 2]] + mat3[[2, 0]]) / s; + let qy = (mat3[[1, 2]] + mat3[[2, 1]]) / s; + let qz = 0.25 * s; + Array::::from_vec(vec![qw, qx, qy, qz]) + }; + + // Canonicalize the quaternion. + if quat_wxyz[0] < 0.0 { + quat_wxyz *= -1.0; + } + quat_wxyz +} + +/// Convert a scalar-first quaternion to yaw. +/// In the Argoverse 2 coordinate system, this is counter-clockwise rotation about the +z axis. +/// Parallelized for batch processing. +pub fn quat_to_yaw(quat_wxyz: &ArrayView) -> Array { + let num_quats = quat_wxyz.shape()[0]; + let mut yaws_rad = Array::::zeros((num_quats, 1)); + par_azip!((mut y in yaws_rad.outer_iter_mut(), q in quat_wxyz.outer_iter()) { + y[0] = _quat_to_yaw(&q); + }); + yaws_rad +} + +/// Convert a scalar-first quaternion to yaw. +/// In the Argoverse 2 coordinate system, this is counter-clockwise rotation about the +z axis. +pub fn _quat_to_yaw(quat_wxyz: &ArrayView) -> f32 { + let (qw, qx, qy, qz) = (quat_wxyz[0], quat_wxyz[1], quat_wxyz[2], quat_wxyz[3]); + let siny_cosp = 2. * (qw * qz + qx * qy); + let cosy_cosp = 1. - 2. * (qy * qy + qz * qz); + siny_cosp.atan2(cosy_cosp) +} + +/// Convert a scalar-first quaternion to yaw. +/// In the Argoverse 2 coordinate system, this is counter-clockwise rotation about the +z axis. +/// Parallelized for batch processing. +pub fn yaw_to_quat(yaw_rad: &ArrayView) -> Array { + let num_yaws = yaw_rad.shape()[0]; + let mut quat_wxyz = Array::::zeros((num_yaws, 4)); + par_azip!((mut q in quat_wxyz.outer_iter_mut(), y in yaw_rad.outer_iter()) { + q.assign(&_yaw_to_quat(y[0])); + }); + quat_wxyz +} + +/// Convert rotation about the z-axis to a scalar-first quaternion. +pub fn _yaw_to_quat(yaw_rad: f32) -> Array { + let qw = f32::cos(0.5 * yaw_rad); + let qz = f32::sin(0.5 * yaw_rad); + Array::::from_vec(vec![qw, 0.0, 0.0, qz]) +} + +/// Reflect orientation across the x-axis. +/// (N,4) `quat_wxyz` orientation of `N` rigid objects. +pub fn reflect_orientation_x(quat_wxyz: &ArrayView) -> Array { + let yaw_rad = quat_to_yaw(quat_wxyz); + let reflected_yaw_rad = -yaw_rad; + yaw_to_quat(&reflected_yaw_rad.view()) +} + +/// Reflect orientation across the y-axis. +/// (N,4) `quat_wxyz` orientation of `N` rigid objects. +pub fn reflect_orientation_y(quat_wxyz: &ArrayView) -> Array { + let yaw_rad = quat_to_yaw(quat_wxyz); + let reflected_yaw_rad = PI - yaw_rad; + yaw_to_quat(&reflected_yaw_rad.view()) +} + +/// Reflect translation across the x-axis. +pub fn reflect_translation_x(xyz_m: &ArrayView) -> Array { + let mut augmented_xyz_m = xyz_m.to_owned(); + augmented_xyz_m + .slice_mut(s![.., 1]) + .par_mapv_inplace(|y| -y); + augmented_xyz_m +} + +/// Reflect translation across the y-axis. +pub fn reflect_translation_y(xyz_m: &ArrayView) -> Array { + let mut augmented_xyz_m = xyz_m.to_owned(); + augmented_xyz_m + .slice_mut(s![.., 0]) + .par_mapv_inplace(|x| -x); + augmented_xyz_m +} + +/// Sample a random quaternion. +pub fn sample_random_quat_wxyz() -> Array { + let distribution = StandardNormal; + let qw: f32 = distribution.sample(&mut rand::thread_rng()); + let qx: f32 = distribution.sample(&mut rand::thread_rng()); + let qy: f32 = distribution.sample(&mut rand::thread_rng()); + let qz: f32 = distribution.sample(&mut rand::thread_rng()); + let quat_wxyz = Array::::from_vec(vec![qw, qx, qy, qz]); + let norm = quat_wxyz.dot(&quat_wxyz).sqrt(); + let mut versor_wxyz = quat_wxyz / norm; + + // Canonicalize the quaternion. + if versor_wxyz[0] < 0.0 { + versor_wxyz *= -1.0; + } + versor_wxyz +} + +#[cfg(test)] +mod tests { + use super::{_mat3_to_quat, _quat_to_mat3, sample_random_quat_wxyz}; + + #[test] + fn test_quat_to_mat3_round_trip() { + let num_trials = 100000; + let epsilon = 1e-6; + for _ in 0..num_trials { + let quat_wxyz = sample_random_quat_wxyz(); + let mat3 = _quat_to_mat3(&quat_wxyz.view()); + let _quat_wxyz = _mat3_to_quat(&mat3.view()); + assert!(quat_wxyz.abs_diff_eq(&_quat_wxyz, epsilon)); + } + } +} diff --git a/rust/src/geometry/utils.rs b/rust/src/geometry/utils.rs new file mode 100644 index 00000000..fcac371a --- /dev/null +++ b/rust/src/geometry/utils.rs @@ -0,0 +1,15 @@ +//! # utils +//! +//! Geometric utilities. + +use ndarray::{s, Array, Ix2}; + +/// Convert Cartesian coordinates into Homogeneous coordinates. +/// This function converts a set of points in R^N to its homogeneous representation in R^(N+1). +pub fn cart_to_hom(cart: Array) -> Array { + let num_points = cart.shape()[0]; + let num_dims = cart.shape()[1]; + let mut hom = Array::::ones([num_points, num_dims + 1]); + hom.slice_mut(s![.., ..num_dims]).assign(&cart); + hom +} diff --git a/rust/src/io.rs b/rust/src/io.rs new file mode 100644 index 00000000..ec6f64d3 --- /dev/null +++ b/rust/src/io.rs @@ -0,0 +1,282 @@ +//! # io +//! +//! Reading and writing operations. + +use image::ImageBuffer; +use image::Rgba; +use ndarray::s; +use ndarray::Array; +use ndarray::Array2; +use ndarray::Array3; +use ndarray::Ix1; + +use nshare::ToNdarray3; +use polars::lazy::dsl::lit; +use polars::lazy::dsl::Expr; +use polars::lazy::dsl::GetOutput; + +use polars::prelude::*; + +use polars::series::Series; +use polars::{ + self, + lazy::dsl::{col, cols}, + prelude::{DataFrame, IntoLazy}, +}; +use rayon::prelude::IntoParallelRefIterator; +use rayon::prelude::ParallelIterator; +use std::fs::File; +use std::path::PathBuf; + +use crate::constants::POSE_COLUMNS; +use crate::geometry::se3::SE3; +use image::io::Reader as ImageReader; + +use crate::geometry::so3::_quat_to_mat3; + +/// Read a feather file and load into a `polars` dataframe. +pub fn read_feather_eager(path: &PathBuf, memory_mapped: bool) -> DataFrame { + let file = + File::open(path).unwrap_or_else(|_| panic!("{path} not found.", path = path.display())); + polars::io::ipc::IpcReader::new(file) + .memory_mapped(memory_mapped) + .finish() + .unwrap() +} + +/// Write a feather file to disk using LZ4 compression. +pub fn write_feather_eager(path: &PathBuf, mut data_frame: DataFrame) { + let file = File::create(path).expect("could not create file"); + IpcWriter::new(file) + .with_compression(Some(IpcCompression::LZ4)) + .finish(&mut data_frame) + .unwrap() +} + +/// Read a feather file and load into a `polars` dataframe. +/// TODO: Implement once upstream half-type is fixed. +// pub fn read_feather_lazy(path: &PathBuf, memory_mapped: bool) -> DataFrame { +// LazyFrame::scan_ipc( +// path, +// ScanArgsIpc { +// n_rows: None, +// cache: true, +// rechunk: true, +// row_count: None, +// memmap: memory_mapped, +// }, +// ) +// .unwrap() +// .collect() +// .unwrap() +// } + +/// Read and accumulate lidar sweeps. +/// Accumulation will only occur if `num_accumulated_sweeps` > 1. +/// Sweeps are motion-compensated to the most recent sweep (i.e., at `timestamp_ns`). +pub fn read_accumulate_lidar( + log_dir: PathBuf, + file_index: &DataFrame, + log_id: &str, + timestamp_ns: u64, + idx: usize, + num_accumulated_sweeps: usize, + memory_mapped: bool, +) -> LazyFrame { + let start_idx = i64::max(idx as i64 - num_accumulated_sweeps as i64 + 1, 0) as usize; + let log_ids = file_index["log_id"].str().unwrap(); + let timestamps = file_index["timestamp_ns"].u64().unwrap(); + let poses_path = log_dir.join("city_SE3_egovehicle.feather"); + let poses = read_feather_eager(&poses_path, memory_mapped); + + let pose_ref = ndarray_filtered_from_frame( + &poses, + cols(POSE_COLUMNS), + col("timestamp_ns").eq(timestamp_ns), + ); + + let translation = pose_ref.slice(s![0, ..3]).as_standard_layout().to_owned(); + let quat_wxyz = pose_ref.slice(s![0, 3..]).as_standard_layout().to_owned(); + let rotation = _quat_to_mat3(&quat_wxyz.view()); + let city_se3_ego = SE3 { + rotation, + translation, + }; + let ego_se3_city = city_se3_ego.inverse(); + let indices: Vec<_> = (start_idx..=idx).collect(); + let mut lidar_list = indices + .par_iter() + .filter_map(|i| { + let log_id_i = log_ids.get(*i).unwrap(); + match log_id_i == log_id { + true => Some(i), + _ => None, + } + }) + .map(|i| { + let timestamp_ns_i = timestamps.get(*i).unwrap(); + let lidar_path = build_lidar_file_path(log_dir.clone(), timestamp_ns_i); + let mut lidar = read_feather_eager(&lidar_path, memory_mapped) + .lazy() + .with_column( + col("x") + .map( + |x| Ok(Some(Series::from_vec("timedelta_ns", vec![0_f32; x.len()]))), + GetOutput::float_type(), + ) + .alias("timedelta_ns"), + ); + if timestamp_ns_i != timestamp_ns { + let xyz = lidar + .clone() + .select(&[cols(["x", "y", "z"])]) + .collect() + .unwrap() + .to_ndarray::(IndexOrder::C) + .unwrap(); + + let pose_i = poses + .clone() + .lazy() + .filter(col("timestamp_ns").eq(lit(timestamp_ns_i))) + .select(&[cols(POSE_COLUMNS)]) + .collect() + .unwrap(); + + let city_se3_ego_i = data_frame_to_se3(pose_i); + let ego_ref_se3_ego_i = ego_se3_city.compose(&city_se3_ego_i); + let xyz_ref = ego_ref_se3_ego_i.transform_from(&xyz.view()); + let x_ref = Series::new("x", xyz_ref.slice(s![.., 0]).to_owned().into_raw_vec()); + let y_ref = Series::new("y", xyz_ref.slice(s![.., 1]).to_owned().into_raw_vec()); + let z_ref = Series::new("z", xyz_ref.slice(s![.., 2]).to_owned().into_raw_vec()); + let timedelta_ns = Series::new( + "timedelta_ns", + vec![(timestamp_ns - timestamp_ns_i) as f32 * 1e-9; xyz.shape()[0]], + ); + lidar = + lidar.with_columns(vec![lit(x_ref), lit(y_ref), lit(z_ref), lit(timedelta_ns)]); + } + lidar + }) + .collect::>(); + + lidar_list.reverse(); + concat(lidar_list, UnionArgs::default()).unwrap() +} + +/// Read a dataframe, but filter for the specified timestamp. +pub fn read_timestamped_feather( + path: &PathBuf, + columns: &Vec<&str>, + timestamp_ns: &u64, + memory_mapped: bool, +) -> LazyFrame { + read_feather_eager(path, memory_mapped) + .lazy() + .filter(col("timestamp_ns").eq(*timestamp_ns)) + .select(&[cols(columns)]) +} + +/// Read an image into an RGBA u8 image. +pub fn read_image_rgba8(path: &PathBuf) -> ImageBuffer, Vec> { + ImageReader::open(path) + .unwrap() + .decode() + .unwrap() + .to_rgba8() +} + +/// Read an image into an RGBA u8 image and convert to `ndarray`. +pub fn read_image_rgba8_ndarray(path: &PathBuf) -> Array3 { + let image = ImageReader::open(path) + .unwrap() + .decode() + .unwrap() + .to_rgba8(); + image.into_ndarray3() +} + +/// Build the lidar file path. +pub fn build_lidar_file_path(log_dir: PathBuf, timestamp_ns: u64) -> PathBuf { + let file_name = format!("{timestamp_ns}.feather"); + let lidar_path = [ + log_dir, + "sensors".to_string().into(), + "lidar".to_string().into(), + file_name.into(), + ] + .iter() + .collect(); + lidar_path +} + +/// Convert a dataframe to `ndarray`. +pub fn ndarray_from_frame(frame: &DataFrame, exprs: Expr) -> Array2 { + frame + .clone() + .lazy() + .select(&[exprs]) + .collect() + .unwrap() + .to_ndarray::(IndexOrder::C) + .unwrap() + .as_standard_layout() + .to_owned() +} + +/// Convert a dataframe to `ndarray` and filter. +pub fn ndarray_filtered_from_frame( + frame: &DataFrame, + select_exprs: Expr, + filter_exprs: Expr, +) -> Array2 { + frame + .clone() + .lazy() + .filter(filter_exprs) + .select(&[select_exprs]) + .collect() + .unwrap() + .to_ndarray::(IndexOrder::C) + .unwrap() + .as_standard_layout() + .to_owned() +} + +/// Convert a data_frame with pose columns into an `se3` object. +pub fn data_frame_to_se3(data_frame: DataFrame) -> SE3 { + let qw = extract_f32_from_data_frame(&data_frame, "qw"); + let qx = extract_f32_from_data_frame(&data_frame, "qx"); + let qy = extract_f32_from_data_frame(&data_frame, "qy"); + let qz = extract_f32_from_data_frame(&data_frame, "qz"); + let tx_m = extract_f32_from_data_frame(&data_frame, "tx_m"); + let ty_m = extract_f32_from_data_frame(&data_frame, "ty_m"); + let tz_m = extract_f32_from_data_frame(&data_frame, "tz_m"); + let quat_wxyz = Array::::from_vec(vec![qw, qx, qy, qz]); + let rotation = _quat_to_mat3(&quat_wxyz.view()); + let translation = Array::::from_vec(vec![tx_m, ty_m, tz_m]); + SE3 { + rotation, + translation, + } +} + +/// Extract an f32 field from a single row data frame. +pub fn extract_f32_from_data_frame(data_frame: &DataFrame, column: &str) -> f32 { + data_frame + .column(column) + .unwrap() + .get(0) + .unwrap() + .try_extract::() + .unwrap() +} + +/// Extract a usize field from a single row data frame. +pub fn extract_usize_from_data_frame(data_frame: &DataFrame, column: &str) -> usize { + data_frame[column] + .get(0) + .unwrap() + .try_extract::() + .unwrap() +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 00000000..26c3abef --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,100 @@ +//! # av2 +//! +//! Argoverse 2 Rust library. + +#![warn(missing_docs)] +#![warn(missing_doc_code_examples)] + +#[cfg(feature = "blas")] +extern crate blas_src; + +pub mod constants; +pub mod data_loader; +pub mod geometry; +pub mod io; +pub mod ops; +pub mod path; +pub mod share; +pub mod structures; + +use data_loader::{DataLoader, Sweep}; +use ndarray::{Dim, Ix1, Ix2}; +use numpy::PyReadonlyArray; +use numpy::{IntoPyArray, PyArray}; +use pyo3::prelude::*; + +use geometry::so3::{_quat_to_mat3, quat_to_yaw, yaw_to_quat}; +use numpy::PyReadonlyArray2; + +use crate::ops::voxelize; + +#[pyfunction] +#[pyo3(name = "voxelize")] +#[allow(clippy::type_complexity)] +fn py_voxelize<'py>( + py: Python<'py>, + indices: PyReadonlyArray2, + features: PyReadonlyArray2, + length: usize, + width: usize, + height: usize, +) -> ( + &'py PyArray>, + &'py PyArray>, + &'py PyArray>, +) { + let (indices, values, counts) = voxelize( + &indices.as_array(), + &features.as_array(), + length, + width, + height, + ); + ( + indices.into_pyarray(py), + values.into_pyarray(py), + counts.into_pyarray(py), + ) +} + +#[pyfunction] +#[pyo3(name = "quat_to_mat3")] +#[allow(clippy::type_complexity)] +fn py_quat_to_mat3<'py>( + py: Python<'py>, + quat_wxyz: PyReadonlyArray, +) -> &'py PyArray { + _quat_to_mat3(&quat_wxyz.as_array().view()).into_pyarray(py) +} + +#[pyfunction] +#[pyo3(name = "quat_to_yaw")] +#[allow(clippy::type_complexity)] +fn py_quat_to_yaw<'py>( + py: Python<'py>, + quat_wxyz: PyReadonlyArray, +) -> &'py PyArray { + quat_to_yaw(&quat_wxyz.as_array().view()).into_pyarray(py) +} + +#[pyfunction] +#[pyo3(name = "yaw_to_quat")] +#[allow(clippy::type_complexity)] +fn py_yaw_to_quat<'py>( + py: Python<'py>, + quat_wxyz: PyReadonlyArray, +) -> &'py PyArray { + yaw_to_quat(&quat_wxyz.as_array().view()).into_pyarray(py) +} + +/// A Python module implemented in Rust. +#[pymodule] +fn _r(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(py_quat_to_mat3, m)?)?; + m.add_function(wrap_pyfunction!(py_quat_to_yaw, m)?)?; + m.add_function(wrap_pyfunction!(py_voxelize, m)?)?; + m.add_function(wrap_pyfunction!(py_yaw_to_quat, m)?)?; + Ok(()) +} diff --git a/rust/src/ops.rs b/rust/src/ops.rs new file mode 100644 index 00000000..7befed03 --- /dev/null +++ b/rust/src/ops.rs @@ -0,0 +1,68 @@ +//! # ops +//! +//! Optimized operations for data processing. + +use itertools::Itertools; +use ndarray::{azip, par_azip, s, Array1, Array2, ArrayView2, Axis}; +use std::{ + collections::HashMap, + ops::{AddAssign, DivAssign}, +}; + +/// Convert unraveled coordinates (i.e., multi-dimensional indices) to a linear index. +pub fn ravel_multi_index(unraveled_coords: &ArrayView2, size: Vec) -> Array2 { + let shape_arr = Array1::::from_vec(size.into_iter().chain(vec![1]).collect::>()); + + let mut coefs = shape_arr + .slice(s![1..]) + .into_iter() + .rev() + .copied() + .collect::>(); + + coefs.accumulate_axis_inplace(Axis(0), |prev, curr| *curr *= prev); + coefs = coefs.iter().rev().copied().collect::>(); + (unraveled_coords * &coefs) + .sum_axis(Axis(1)) + .insert_axis(Axis(1)) +} + +/// Cluster a group of features into a set of voxels based on their indices. +pub fn voxelize( + indices: &ArrayView2, + features: &ArrayView2, + length: usize, + width: usize, + height: usize, +) -> (Array2, Array2, Array2) { + let shape = vec![length, width, height]; + let raveled_indices = ravel_multi_index(&indices.view(), shape); + + let num_features = features.shape()[1]; + + let unique_indices = raveled_indices + .clone() + .into_raw_vec() + .into_iter() + .unique() + .enumerate() + .map(|(k, v)| (v, k)) + .collect::>(); + + let mut indices_buffer = Array2::::zeros([unique_indices.len(), 3]); + let mut values_buffer = Array2::::zeros([unique_indices.len(), num_features]); + let mut counts = Array2::::zeros([unique_indices.len(), 1]); + + azip!((idx in raveled_indices.rows(), index in indices.rows(), val in features.rows()) { + let i = *unique_indices.get(&idx[0]).unwrap(); + + indices_buffer.slice_mut(s![i, ..]).assign(&index); + values_buffer.slice_mut(s![i, ..]).add_assign(&val); + counts[[i, 0]] += 1.; + }); + + par_azip!((mut val in values_buffer.rows_mut(), count in counts.rows()) { + val.div_assign(&count); + }); + (indices_buffer, values_buffer, counts) +} diff --git a/rust/src/path.rs b/rust/src/path.rs new file mode 100644 index 00000000..0d31a0f6 --- /dev/null +++ b/rust/src/path.rs @@ -0,0 +1,28 @@ +//! # path +//! +//! File path traversal utilities. + +use anyhow::{Context, Result}; +use std::fs; +use std::path::Path; +use std::path::PathBuf; + +/// Walk a directory and filter invalid paths. +pub fn walk_dir(dir: &PathBuf) -> Result> { + let files: Vec<_> = fs::read_dir(dir)? + .filter_map(|x| x.ok()) + .map(|x| x.path()) + .collect(); + Ok(files) +} + +/// Extract the file stem from a path. +pub fn extract_file_stem(dir: &Path) -> Result { + let file_stem = dir + .file_stem() + .context("Cannot parse file stem.")? + .to_str() + .context("Cannot convert file stem to string.")? + .to_string(); + Ok(file_stem) +} diff --git a/rust/src/share.rs b/rust/src/share.rs new file mode 100644 index 00000000..3e69bfde --- /dev/null +++ b/rust/src/share.rs @@ -0,0 +1,42 @@ +//! # share +//! +//! Conversion methods between different libraries. + +use ndarray::{Array, Ix2}; +use polars::{ + lazy::dsl::{cols, lit, Expr}, + prelude::{DataFrame, Float32Type, IndexOrder, IntoLazy, NamedFrom}, + series::Series, +}; + +/// Convert the columns of an `ndarray::Array` into a vector of `polars` expressions. +pub fn ndarray_to_expr_vec(arr: Array, column_names: Vec<&str>) -> Vec { + let num_dims = arr.shape()[1]; + if num_dims != column_names.len() { + panic!("Number of array columns and column names must match."); + } + + let mut series_vec = vec![]; + for (column, column_name) in arr.columns().into_iter().zip(column_names) { + let series = Series::new( + column_name, + column.as_standard_layout().to_owned().into_raw_vec(), + ); + series_vec.push(lit(series)); + } + series_vec +} + +/// Convert a data frame to an `ndarray::Array::`. +pub fn data_frame_to_ndarray_f32( + data_frame: DataFrame, + column_names: Vec<&str>, +) -> Array { + data_frame + .lazy() + .select(&[cols(column_names)]) + .collect() + .unwrap() + .to_ndarray::(IndexOrder::C) + .unwrap() +} diff --git a/rust/src/structures/mod.rs b/rust/src/structures/mod.rs new file mode 100644 index 00000000..70897ccd --- /dev/null +++ b/rust/src/structures/mod.rs @@ -0,0 +1,6 @@ +//! # structures +//! +//! Structures for sensor data. + +/// Image at a particular timestamp in nanoseconds. +pub mod timestamped_image; diff --git a/rust/src/structures/timestamped_image.rs b/rust/src/structures/timestamped_image.rs new file mode 100644 index 00000000..34adb751 --- /dev/null +++ b/rust/src/structures/timestamped_image.rs @@ -0,0 +1,14 @@ +use image::{ImageBuffer, Rgba}; + +use crate::geometry::camera::pinhole_camera::PinholeCamera; + +/// Image modeled by `camera_model` occuring at `timestamp_ns`. +#[derive(Clone)] +pub struct TimeStampedImage { + /// RGBA u8 image buffer. + pub image: ImageBuffer, Vec>, + /// Pinhole camera model with intrinsics and extrinsics. + pub camera_model: PinholeCamera, + /// Nanosecond timestamp. + pub timestamp_ns: usize, +} diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2f85e4d8..00000000 --- a/setup.cfg +++ /dev/null @@ -1,46 +0,0 @@ -[metadata] -author = Argo AI -author_email = argoverse-api@argo.ai -description_file = README.md -license = MIT -name = av2 -url = https://github.com/argoai/av2-api -version = attr: av2.__version__ -long_description = file: README.md -long_description_content_type = text/markdown -classifiers = - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - -[options] -zip_safe = False -include_package_data = True -python_requires = >= 3.8 -install_requires = - av - click - joblib - matplotlib - nox - numba - numpy>=1.21.5 - opencv-python - pandas - pyarrow - pyproj - rich - scipy - -package_dir= - =src -packages=find: - -[options.packages.find] -where=src - -[flake8] -enable_extensions = G -exclude = build,.nox,.pytype -ignore = ANN101,ANN102,E201,E203,E241,E704,E711,E722,E741,W291,W293,W391,W503,F821,F401,F811,F841,P101,G004,G002,I201,I100,I101 -max_line_length = 120 diff --git a/src/av2/__init__.py b/src/av2/__init__.py index 39ec1a63..78b13f17 100644 --- a/src/av2/__init__.py +++ b/src/av2/__init__.py @@ -1,5 +1,3 @@ # """Argoverse 2 API.""" - -__version__ = "0.2.0" diff --git a/src/av2/_r.pyi b/src/av2/_r.pyi new file mode 100644 index 00000000..2000aba6 --- /dev/null +++ b/src/av2/_r.pyi @@ -0,0 +1,29 @@ +"""Rust backend typing stubs.""" + +from dataclasses import dataclass, field +from typing import List, Optional, Tuple + +import polars as pl +import torch + +@dataclass +class DataLoader: + root_dir: str + dataset_name: str + dataset_type: str + split_name: str + num_accumulated_sweeps: int + memory_map: bool + + file_index: pl.DataFrame = field(init=False) + + def get(self, index: int) -> Sweep: ... + def get_synchronized_images(self, index: int) -> List[torch.Tensor]: ... + def __len__(self) -> int: ... + +@dataclass +class Sweep: + city_pose: pl.DataFrame + lidar: pl.DataFrame + sweep_uuid: Tuple[str, int] + cuboids: Optional[pl.DataFrame] diff --git a/src/av2/datasets/lidar/README.md b/src/av2/datasets/lidar/README.md deleted file mode 100644 index 3e590585..00000000 --- a/src/av2/datasets/lidar/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Argoverse 2 Lidar Dataset Overview - -

- - - - -

- - -The Argoverse 2 Lidar Dataset is intended to support research into self-supervised learning in the lidar domain as well as point cloud forecasting. The AV2 Lidar Dataset is mined with the same criteria as the Forecasting Dataset to ensure that each scene is interesting. While the Lidar Dataset does not have 3D object annotations, each scenario carries an HD map with rich, 3D information about the scene. - -## Dataset Size - -Our dataset is the largest such collection to date with 20,000 thirty second sequences. - -## Sensor Suite - -Lidar sweeps are collected at 10 Hz. In addition, 6-DOF ego-vehicle pose in a global coordinate system are provided. Lidar returns are captured by two 32-beam lidars, spinning at 10 Hz in the same direction, but separated in orientation by 180°. - -We aggregate all returns from the two stacked 32-beam sensors into a single sweep. These sensors each have different, overlapping fields-of-view. Both lidars have their own reference frame, and we refer to them as `up_lidar` and `down_lidar`, respectively. We have egomotion-compensated the lidar sensor data to the egovehicle reference nanosecond timestamp. **All lidar returns are provided in the egovehicle reference frame, not the individual lidar reference frame**. - -## Dataset Structure Format - -Tabular data (lidar sweeps, poses) are provided as [Apache Feather Files](https://arrow.apache.org/docs/python/feather.html) with the file extension `.feather`. - -**Maps:** A local vector map is provided per log, please refer to the [Map README](../../map/README.md) for additional details. - -Directory structure: -``` -av2 -└───lidar - └───train - | └───LyIXwbWeHWPHYUZjD1JPdXcvvtYumCWG - | └───sensors - | | └───lidar - | | └───15970913559644000.feather - | | . - | | . - | | . - | └───calibration - | | └───egovehicle_SE3_sensor.feather - | └───map - | | └───log_map_archive_LyIXwbWeHWPHYUZjD1JPdXcvvtYumCWG__Summer____PIT_city_77257.json - | └───city_SE3_egovehicle.feather - └───val - └───test -``` - -An example sweep `sensors/lidar/15970913559644000.feather`, meaning a reference timestamp of 15970913559644000 nanoseconds: -```python - x y z intensity laser_number offset_ns -0 -1.291016 2.992188 -0.229370 24 31 3318000 -1 -25.921875 25.171875 0.992188 5 14 3318000 -2 -15.500000 18.937500 0.901855 34 16 3320303 -3 -3.140625 4.593750 -0.163696 12 30 3320303 -4 -4.445312 6.535156 -0.109802 14 29 3322607 -... ... ... ... ... ... ... -98231 18.312500 -38.187500 3.279297 26 50 106985185 -98232 23.109375 -34.437500 3.003906 20 49 106987490 -98233 4.941406 -5.777344 -0.162720 12 32 106987490 -98234 6.640625 -8.257812 -0.157593 6 33 106989794 -98235 20.015625 -37.062500 2.550781 12 47 106989794 - -[98236 rows x 6 columns] -``` - -## Lidar Dataset splits -We randomly partition the dataset into the following splits: - -- Train (16,000 logs) -- Validation (2,000 logs) -- Test (2,000 logs) diff --git a/src/av2/datasets/motion_forecasting/README.md b/src/av2/datasets/motion_forecasting/README.md deleted file mode 100644 index 5ddc7058..00000000 --- a/src/av2/datasets/motion_forecasting/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Argoverse 2 Motion Forecasting - -![](https://user-images.githubusercontent.com/29715011/158486284-1a0df794-ee0a-4ae6-a320-0dd0d1daad06.gif) -![](https://user-images.githubusercontent.com/29715011/158486286-e734e654-b879-4994-a129-9957cc591af4.gif) -![](https://user-images.githubusercontent.com/29715011/158486288-5e7c0971-de0c-4ff5-bea7-76f7922dd1e0.gif) - -## Overview - -The Argoverse 2.0 motion forecasting dataset consists of 250,000 scenarios, collected from 6 cities spanning multiple seasons. - -Each scenario is specifically designed to maximize interactions relevant to the ego-vehicle. This naturally results in the inclusion of actor-dense scenes featuring a range of vehicle and non-vehicle actor types. At the time of release, AV2 provides the largest object taxonomy, in addition to the broadest mapped area of any motion forecasting dataset released so far. - -## Download - -The latest version of the AV2 motion forecasting dataset can be downloaded from the Argoverse [website](https://www.argoverse.org/av2.html). - -## Scenarios and Tracks - -Each scenario is 11s long and consists of a collection of actor histories, which are represented as "tracks". For each scenario, we provide the following high-level attributes: - -- `scenario_id`: Unique ID associated with this scenario. -- `timestamps_ns`: All timestamps associated with this scenario. -- `tracks`: All tracks associated with this scenario. -- `focal_track_id`: The track ID associated with the focal agent of the scenario. -- `city_name`: The name of the city associated with this scenario. - -Each track is further associated with the following attributes: - -- `track_id`: Unique ID associated with this track -- `object_states`: States for each timestep where the track object had a valid observation. -- `object_type`: Inferred type for the track object. -- `category`: Assigned category for track - used as an indicator for prediction requirements and data quality. - -Track object states bundle all information associated with a particular actor at a fixed point in time: - -- `observed`: Boolean indicating if this object state falls in the observed segment of the scenario. -- `timestep`: Time step corresponding to this object state [0, num_scenario_timesteps). -- `position`: (x, y) Coordinates of center of object bounding box. -- `heading`: Heading associated with object bounding box (in radians, defined w.r.t the map coordinate frame). -- `velocity`: (x, y) Instantaneous velocity associated with the object (in m/s). - -Each track is assigned one of the following labels, which dictate scoring behavior in the Argoverse challenges: - -- `TRACK_FRAGMENT`: Lower quality track that may only contain a few timestamps of observations. -- `UNSCORED_TRACK`: Unscored track used for contextual input. -- `SCORED_TRACK`: High-quality tracks relevant to the AV - scored in the multi-agent prediction challenge. -- `FOCAL_TRACK`: The primary track of interest in a given scenario - scored in the single-agent prediction challenge. - -Each track is also assigned one of the following labels, as part of the 10-class object taxonomy: - -- Dynamic - - `VEHICLE` - - `PEDESTRIAN` - - `MOTORCYCLIST` - - `CYCLIST` - - `BUS` -- Static - - `STATIC` - - `BACKGROUND` - - `CONSTRUCTION` - - `RIDERLESS_BICYCLE` -- `UNKNOWN` - -For more additional details regarding the data schema, please refer [here](data_schema.py). - -## Visualization - -Motion forecasting scenarios can be visualized using the viz [`script`](../../../../tutorials/generate_forecasting_scenario_visualizations.py) or by calling the viz [`library`](viz/scenario_visualization.py#L48) directly. diff --git a/src/av2/datasets/motion_forecasting/eval/metrics.py b/src/av2/datasets/motion_forecasting/eval/metrics.py index 961ecf75..d8424912 100644 --- a/src/av2/datasets/motion_forecasting/eval/metrics.py +++ b/src/av2/datasets/motion_forecasting/eval/metrics.py @@ -1,12 +1,16 @@ # """Utilities to evaluate motion forecasting predictions and compute metrics.""" +from typing import List + import numpy as np -from av2.utils.typing import NDArrayBool, NDArrayFloat, NDArrayNumber +from av2.utils.typing import NDArrayBool, NDArrayFloat -def compute_ade(forecasted_trajectories: NDArrayNumber, gt_trajectory: NDArrayNumber) -> NDArrayFloat: +def compute_ade( + forecasted_trajectories: NDArrayFloat, gt_trajectory: NDArrayFloat +) -> NDArrayFloat: """Compute the average displacement error for a set of K predicted trajectories (for the same actor). Args: @@ -16,12 +20,16 @@ def compute_ade(forecasted_trajectories: NDArrayNumber, gt_trajectory: NDArrayNu Returns: (K,) Average displacement error for each of the predicted trajectories. """ - displacement_errors = np.linalg.norm(forecasted_trajectories - gt_trajectory, axis=2) # type: ignore + displacement_errors = np.linalg.norm( + forecasted_trajectories - gt_trajectory, axis=2 + ) ade: NDArrayFloat = np.mean(displacement_errors, axis=1) return ade -def compute_fde(forecasted_trajectories: NDArrayNumber, gt_trajectory: NDArrayNumber) -> NDArrayFloat: +def compute_fde( + forecasted_trajectories: NDArrayFloat, gt_trajectory: NDArrayFloat +) -> NDArrayFloat: """Compute the final displacement error for a set of K predicted trajectories (for the same actor). Args: @@ -32,14 +40,15 @@ def compute_fde(forecasted_trajectories: NDArrayNumber, gt_trajectory: NDArrayNu (K,) Final displacement error for each of the predicted trajectories. """ # Compute final displacement error for all K trajectories - fde_vector = (forecasted_trajectories - gt_trajectory)[:, -1] # type: ignore - fde: NDArrayFloat = np.linalg.norm(fde_vector, axis=-1) # type: ignore + error_vector: NDArrayFloat = forecasted_trajectories - gt_trajectory + fde_vector = error_vector[:, -1] + fde: NDArrayFloat = np.linalg.norm(fde_vector, axis=-1) return fde def compute_is_missed_prediction( - forecasted_trajectories: NDArrayNumber, - gt_trajectory: NDArrayNumber, + forecasted_trajectories: NDArrayFloat, + gt_trajectory: NDArrayFloat, miss_threshold_m: float = 2.0, ) -> NDArrayBool: """Compute whether each of K predicted trajectories (for the same actor) missed by more than a distance threshold. @@ -50,17 +59,17 @@ def compute_is_missed_prediction( miss_threshold_m: Minimum distance threshold for final displacement to be considered a miss. Returns: - (K,) Bools indicating whether prediction missed by more than specified threshold. + (K,) bools indicating whether prediction missed by more than specified threshold. """ fde = compute_fde(forecasted_trajectories, gt_trajectory) - is_missed_prediction = fde > miss_threshold_m # type: ignore + is_missed_prediction: NDArrayBool = fde > miss_threshold_m return is_missed_prediction def compute_brier_ade( - forecasted_trajectories: NDArrayNumber, - gt_trajectory: NDArrayNumber, - forecast_probabilities: NDArrayNumber, + forecasted_trajectories: NDArrayFloat, + gt_trajectory: NDArrayFloat, + forecast_probabilities: NDArrayFloat, normalize: bool = False, ) -> NDArrayFloat: """Compute a probability-weighted (using Brier score) ADE for K predicted trajectories (for the same actor). @@ -75,16 +84,18 @@ def compute_brier_ade( (K,) Probability-weighted average displacement error for each predicted trajectory. """ # Compute ADE with Brier score component - brier_score = _compute_brier_score(forecasted_trajectories, forecast_probabilities, normalize) + brier_score = _compute_brier_score( + forecasted_trajectories, forecast_probabilities, normalize + ) ade_vector = compute_ade(forecasted_trajectories, gt_trajectory) brier_ade: NDArrayFloat = ade_vector + brier_score return brier_ade def compute_brier_fde( - forecasted_trajectories: NDArrayNumber, - gt_trajectory: NDArrayNumber, - forecast_probabilities: NDArrayNumber, + forecasted_trajectories: NDArrayFloat, + gt_trajectory: NDArrayFloat, + forecast_probabilities: NDArrayFloat, normalize: bool = False, ) -> NDArrayFloat: """Compute a probability-weighted (using Brier score) FDE for K predicted trajectories (for the same actor). @@ -99,15 +110,17 @@ def compute_brier_fde( (K,) Probability-weighted final displacement error for each predicted trajectory. """ # Compute FDE with Brier score component - brier_score = _compute_brier_score(forecasted_trajectories, forecast_probabilities, normalize) + brier_score = _compute_brier_score( + forecasted_trajectories, forecast_probabilities, normalize + ) fde_vector = compute_fde(forecasted_trajectories, gt_trajectory) brier_fde: NDArrayFloat = fde_vector + brier_score return brier_fde def _compute_brier_score( - forecasted_trajectories: NDArrayNumber, - forecast_probabilities: NDArrayNumber, + forecasted_trajectories: NDArrayFloat, + forecast_probabilities: NDArrayFloat, normalize: bool = False, ) -> NDArrayFloat: """Compute Brier score for K predicted trajectories. @@ -133,7 +146,9 @@ def _compute_brier_score( # Validate that all forecast probabilities are in the range [0, 1] if np.logical_or(forecast_probabilities < 0.0, forecast_probabilities > 1.0).any(): - raise ValueError("At least one forecast probability falls outside the range [0, 1].") + raise ValueError( + "At least one forecast probability falls outside the range [0, 1]." + ) # If enabled, normalize forecast probabilities to sum to 1 if normalize: @@ -141,3 +156,137 @@ def _compute_brier_score( brier_score: NDArrayFloat = np.square((1 - forecast_probabilities)) return brier_score + + +def compute_world_fde( + forecasted_world_trajectories: NDArrayFloat, gt_world_trajectories: NDArrayFloat +) -> NDArrayFloat: + """Compute the mean final displacement error for each of K predicted worlds. + + Args: + forecasted_world_trajectories: (M, K, N, 2) K predicted trajectories of length N, for each of M actors. + gt_world_trajectories: (M, N, 2) ground truth trajectories of length N, for each of M actors. + + Returns: + (K,) Mean final displacement error for each of the predicted worlds. + """ + actor_fdes = [ + compute_fde(forecasted_actor_trajectories, gt_actor_trajectory) + for forecasted_actor_trajectories, gt_actor_trajectory in zip( + forecasted_world_trajectories, gt_world_trajectories + ) + ] + + world_fdes: NDArrayFloat = np.stack(actor_fdes).mean(axis=0) + return world_fdes + + +def compute_world_ade( + forecasted_world_trajectories: NDArrayFloat, gt_world_trajectories: NDArrayFloat +) -> NDArrayFloat: + """Compute the mean average displacement error for each of K predicted worlds. + + Args: + forecasted_world_trajectories: (M, K, N, 2) K predicted trajectories of length N, for each of M actors. + gt_world_trajectories: (M, N, 2) ground truth trajectories of length N, for each of M actors. + + Returns: + (K,) Mean average displacement error for each of the predicted worlds. + """ + actor_ades = [ + compute_ade(forecasted_actor_trajectories, gt_actor_trajectory) + for forecasted_actor_trajectories, gt_actor_trajectory in zip( + forecasted_world_trajectories, gt_world_trajectories + ) + ] + + world_ades: NDArrayFloat = np.stack(actor_ades).mean(axis=0) + return world_ades + + +def compute_world_misses( + forecasted_world_trajectories: NDArrayFloat, + gt_world_trajectories: NDArrayFloat, + miss_threshold_m: float = 2.0, +) -> NDArrayBool: + """For each world, compute whether predictions for each actor misssed by more than a distance threshold. + + Args: + forecasted_world_trajectories: (M, K, N, 2) K predicted trajectories of length N, for each of M actors. + gt_world_trajectories: (M, N, 2) ground truth trajectories of length N, for each of M actors. + miss_threshold_m: Minimum distance threshold for final displacement to be considered a miss. + + Returns: + (M, K) bools indicating whether prediction missed for actor in each world. + """ + actor_fdes = [ + compute_fde(forecasted_actor_trajectories, gt_actor_trajectory) + for forecasted_actor_trajectories, gt_actor_trajectory in zip( + forecasted_world_trajectories, gt_world_trajectories + ) + ] + + world_actor_missed: NDArrayBool = np.stack(actor_fdes) > miss_threshold_m + return world_actor_missed + + +def compute_world_brier_fde( + forecasted_world_trajectories: NDArrayFloat, + gt_world_trajectories: NDArrayFloat, + forecasted_world_probabilities: NDArrayFloat, + normalize: bool = False, +) -> NDArrayFloat: + """Compute the mean final displacement error for each of K predicted worlds. + + Args: + forecasted_world_trajectories: (M, K, N, 2) K predicted trajectories of length N, for each of M actors. + gt_world_trajectories: (M, N, 2) ground truth trajectories of length N, for each of M actors. + forecasted_world_probabilities: (K,) normalized probabilities associated with each world. + normalize: Normalizes `forecasted_world_probabilities` to sum to 1 when set to True. + + Returns: + (K,) Mean probability-weighted final displacement error for each of the predicted worlds. + """ + actor_brier_fdes = [ + compute_brier_fde( + forecasted_actor_trajectories, + gt_actor_trajectory, + forecasted_world_probabilities, + normalize, + ) + for forecasted_actor_trajectories, gt_actor_trajectory in zip( + forecasted_world_trajectories, gt_world_trajectories + ) + ] + + world_brier_fdes: NDArrayFloat = np.stack(actor_brier_fdes).mean(axis=0) + return world_brier_fdes + + +def compute_world_collisions( + forecasted_world_trajectories: NDArrayFloat, collision_threshold_m: float = 1.0 +) -> NDArrayBool: + """Compute whether any of the forecasted trajectories collide with each other. + + Args: + forecasted_world_trajectories: (M, K, N, 2) K predicted trajectories of length N, for each of M actors. + collision_threshold_m: Distance threshold at which point a collision is considered to have occured. + + Returns: + (M, K) bools indicating if a collision was present for an actor in each predicted world. + """ + actor_collisions: List[NDArrayBool] = [] + for actor_idx in range(len(forecasted_world_trajectories)): + # Compute distance from current actor to all other predicted actors at each timestep + forecasted_actor_trajectories = forecasted_world_trajectories[actor_idx] + scenario_actor_dists = np.linalg.norm( + forecasted_world_trajectories - forecasted_actor_trajectories, axis=-1 + ) + + # For each world, find the closest distance to any other predicted actor, at any time + scenario_actor_dists[actor_idx, :, :] = np.inf + closest_dist_to_other_actor_m = scenario_actor_dists.min(axis=-1).min(axis=0) + actor_collided_in_world = closest_dist_to_other_actor_m < collision_threshold_m + actor_collisions.append(actor_collided_in_world) + + return np.stack(actor_collisions) diff --git a/src/av2/datasets/motion_forecasting/eval/submission.py b/src/av2/datasets/motion_forecasting/eval/submission.py index dab295c9..f748283c 100644 --- a/src/av2/datasets/motion_forecasting/eval/submission.py +++ b/src/av2/datasets/motion_forecasting/eval/submission.py @@ -1,10 +1,8 @@ # - """Classes and utilities used to build submissions for the AV2 motion forecasting challenge.""" from __future__ import annotations -from collections import defaultdict from dataclasses import dataclass from pathlib import Path from typing import Dict, Final, List, Tuple @@ -16,11 +14,12 @@ from av2.utils.typing import NDArrayNumber # Define type aliases used for submission -PredictedTrajectories = NDArrayNumber # (K, AV2_SCENARIO_PRED_TIMESTEPS, 2) -PredictionProbabilities = NDArrayNumber # (K,) -TrackPredictions = Tuple[PredictedTrajectories, PredictionProbabilities] -ScenarioPredictions = Dict[str, TrackPredictions] # Mapping from track ID to track predictions -PredictionRow = Tuple[str, str, float, PredictedTrajectories, PredictionProbabilities] +ScenarioProbabilities = NDArrayNumber # (K,) per-scenario probabilities, one value for each predicted future. +TrackTrajectories = NDArrayNumber # (K, AV2_SCENARIO_PRED_TIMESTEPS, 2) per-track predicted trajectories. + +ScenarioTrajectories = Dict[str, TrackTrajectories] +ScenarioPredictions = Tuple[ScenarioProbabilities, ScenarioTrajectories] +PredictionRow = Tuple[str, str, float, TrackTrajectories, ScenarioProbabilities] SUBMISSION_COL_NAMES: Final[List[str]] = [ "scenario_id", @@ -47,34 +46,37 @@ def __post_init__(self) -> None: Raises: ValueError: If predictions for at least one track are not of shape (*, AV2_SCENARIO_PRED_TIMESTEPS, 2). - ValueError: If for any track, prediction probabilities doesn't match the number of predicted trajectories. - ValueError: If prediction probabilities for at least one track do not sum to 1. + ValueError: If for any track, number of probabilities doesn't match the number of predicted trajectories. + ValueError: If prediction probabilities for at least one scenario do not sum to 1. """ - for scenario_id, scenario_predictions in self.predictions.items(): - for track_id, (predicted_trajectories, prediction_probabilities) in scenario_predictions.items(): + for scenario_id, ( + scenario_probabilities, + scenario_trajectories, + ) in self.predictions.items(): + for track_id, track_trajectories in scenario_trajectories.items(): # Validate that predicted trajectories are of the correct shape - if predicted_trajectories[0].shape[-2:] != EXPECTED_PREDICTION_SHAPE: + if track_trajectories[0].shape[-2:] != EXPECTED_PREDICTION_SHAPE: raise ValueError( f"Prediction for track {track_id} in {scenario_id} found with invalid shape " - f"{predicted_trajectories.shape}, expected (*, {AV2_SCENARIO_PRED_TIMESTEPS}, 2)." + f"{track_trajectories.shape}, expected (*, {AV2_SCENARIO_PRED_TIMESTEPS}, 2)." ) # Validate that the number of predicted trajectories and prediction probabilities matches - if len(predicted_trajectories) != len(prediction_probabilities): + if len(track_trajectories) != len(scenario_probabilities): raise ValueError( f"Prediction for track {track_id} in {scenario_id} has " - f"{len(predicted_trajectories)} predicted trajectories, but " - f"{len(prediction_probabilities)} probabilities." + f"{len(track_trajectories)} predicted trajectories, but " + f"{len(scenario_probabilities)} probabilities." ) - # Validate that prediction probabilities for each track are normalized - prediction_probability_sum = np.sum(prediction_probabilities) - probability_is_normalized = np.isclose(1, prediction_probability_sum) - if not probability_is_normalized: - raise ValueError( - f"Track probabilities must sum to 1, but probabilities for track {track_id} in {scenario_id} " - f"sum up to {prediction_probability_sum}." - ) + # Validate that prediction probabilities for each scenario are normalized + prediction_probability_sum = np.sum(scenario_probabilities) + probability_is_normalized = np.isclose(1, prediction_probability_sum) + if not probability_is_normalized: + raise ValueError( + f"Track probabilities must sum to 1, but probabilities for track {track_id} in {scenario_id} " + f"sum up to {prediction_probability_sum}." + ) @classmethod def from_parquet(cls, submission_file_path: Path) -> ChallengeSubmission: @@ -91,14 +93,28 @@ def from_parquet(cls, submission_file_path: Path) -> ChallengeSubmission: submission_df.sort_values(by="probability", inplace=True, ascending=False) # From serialized data, build scenario-track mapping for predictions - submission_dict: Dict[str, ScenarioPredictions] = defaultdict(lambda: defaultdict(dict)) # type: ignore - for (scenario_id, track_id), track_df in submission_df.groupby(["scenario_id", "track_id"]): - predicted_trajectories_x = np.stack(track_df.loc[:, "predicted_trajectory_x"].values.tolist()) - predicted_trajectories_y = np.stack(track_df.loc[:, "predicted_trajectory_y"].values.tolist()) - predicted_trajectories = np.stack((predicted_trajectories_x, predicted_trajectories_y), axis=-1) - prediction_probabilities = np.array(track_df.loc[:, "probability"].values.tolist()) - - submission_dict[scenario_id][track_id] = (predicted_trajectories, prediction_probabilities) + submission_dict: Dict[str, ScenarioPredictions] = {} + for scenario_id, scenario_df in submission_df.groupby("scenario_id"): + scenario_trajectories: ScenarioTrajectories = {} + for track_id, track_df in scenario_df.groupby("track_id"): + predicted_trajectories_x = np.stack( + track_df.loc[:, "predicted_trajectory_x"].values.tolist() + ) + predicted_trajectories_y = np.stack( + track_df.loc[:, "predicted_trajectory_y"].values.tolist() + ) + predicted_trajectories = np.stack( + (predicted_trajectories_x, predicted_trajectories_y), axis=-1 + ) + scenario_trajectories[track_id] = predicted_trajectories + + scenario_probabilities = np.array( + track_df.loc[:, "probability"].values.tolist() + ) + submission_dict[scenario_id] = ( + scenario_probabilities, + scenario_trajectories, + ) return cls(predictions=submission_dict) @@ -111,16 +127,19 @@ def to_parquet(self, submission_file_path: Path) -> None: prediction_rows: List[PredictionRow] = [] # Build list of rows for the submission dataframe - for scenario_id, scenario_predictions in self.predictions.items(): - for track_id, (predicted_trajectories, prediction_probabilities) in scenario_predictions.items(): - for prediction_idx in range(len(predicted_trajectories)): + for scenario_id, ( + scenario_probabilities, + scenario_trajectories, + ) in self.predictions.items(): + for track_id, track_trajectories in scenario_trajectories.items(): + for world_idx in range(len(track_trajectories)): prediction_rows.append( ( scenario_id, track_id, - prediction_probabilities[prediction_idx], - predicted_trajectories[prediction_idx, :, 0], - predicted_trajectories[prediction_idx, :, 1], + scenario_probabilities[world_idx], + track_trajectories[world_idx, :, 0], + track_trajectories[world_idx, :, 1], ) ) diff --git a/src/av2/datasets/motion_forecasting/scenario_serialization.py b/src/av2/datasets/motion_forecasting/scenario_serialization.py index bb38c82a..a766521e 100644 --- a/src/av2/datasets/motion_forecasting/scenario_serialization.py +++ b/src/av2/datasets/motion_forecasting/scenario_serialization.py @@ -8,10 +8,18 @@ import numpy as np import pandas as pd -from av2.datasets.motion_forecasting.data_schema import ArgoverseScenario, ObjectState, ObjectType, Track, TrackCategory - - -def serialize_argoverse_scenario_parquet(save_path: Path, scenario: ArgoverseScenario) -> None: +from av2.datasets.motion_forecasting.data_schema import ( + ArgoverseScenario, + ObjectState, + ObjectType, + Track, + TrackCategory, +) + + +def serialize_argoverse_scenario_parquet( + save_path: Path, scenario: ArgoverseScenario +) -> None: """Serialize a single Argoverse scenario in parquet format and save to disk. Args: @@ -64,7 +72,9 @@ def load_argoverse_scenario_parquet(scenario_path: Path) -> ArgoverseScenario: # Interpolate scenario timestamps based on the saved start and end timestamps timestamps_ns = np.linspace( - tracks_df["start_timestamp"][0], tracks_df["end_timestamp"][0], num=tracks_df["num_timestamps"][0] + tracks_df["start_timestamp"][0], + tracks_df["end_timestamp"][0], + num=tracks_df["num_timestamps"][0], ) return ArgoverseScenario( @@ -90,7 +100,6 @@ def _convert_tracks_to_tabular_format(tracks: List[Track]) -> pd.DataFrame: track_dfs: List[pd.DataFrame] = [] for track in tracks: - track_df = pd.DataFrame() observed_states: List[bool] = [] @@ -138,10 +147,11 @@ def _load_tracks_from_tabular_format(tracks_df: pd.DataFrame) -> List[Track]: tracks: List[Track] = [] for track_id, track_df in tracks_df.groupby("track_id"): - observed_states: List[bool] = track_df.loc[:, "observed"].values.tolist() object_type: ObjectType = ObjectType(track_df["object_type"].iloc[0]) - object_category: TrackCategory = TrackCategory(track_df["object_category"].iloc[0]) + object_category: TrackCategory = TrackCategory( + track_df["object_category"].iloc[0] + ) timesteps: List[int] = track_df.loc[:, "timestep"].values.tolist() positions: List[Tuple[float, float]] = list( zip( @@ -170,7 +180,12 @@ def _load_tracks_from_tabular_format(tracks_df: pd.DataFrame) -> List[Track]: ) tracks.append( - Track(track_id=track_id, object_states=object_states, object_type=object_type, category=object_category) + Track( + track_id=track_id, + object_states=object_states, + object_type=object_type, + category=object_category, + ) ) return tracks diff --git a/src/av2/datasets/motion_forecasting/viz/scenario_visualization.py b/src/av2/datasets/motion_forecasting/viz/scenario_visualization.py index 9113e82f..4a0fa1ad 100644 --- a/src/av2/datasets/motion_forecasting/viz/scenario_visualization.py +++ b/src/av2/datasets/motion_forecasting/viz/scenario_visualization.py @@ -13,7 +13,11 @@ from PIL import Image as img from PIL.Image import Image -from av2.datasets.motion_forecasting.data_schema import ArgoverseScenario, ObjectType, TrackCategory +from av2.datasets.motion_forecasting.data_schema import ( + ArgoverseScenario, + ObjectType, + TrackCategory, +) from av2.map.map_api import ArgoverseStaticMap from av2.utils.typing import NDArrayFloat, NDArrayInt @@ -35,7 +39,9 @@ _DEFAULT_ACTOR_COLOR: Final[str] = "#D3E8EF" _FOCAL_AGENT_COLOR: Final[str] = "#ECA25B" _AV_COLOR: Final[str] = "#007672" -_BOUNDING_BOX_ZORDER: Final[int] = 100 # Ensure actor bounding boxes are plotted on top of all map elements +_BOUNDING_BOX_ZORDER: Final[int] = ( + 100 # Ensure actor bounding boxes are plotted on top of all map elements +) _STATIC_OBJECT_TYPES: Set[ObjectType] = { ObjectType.STATIC, @@ -45,7 +51,11 @@ } -def visualize_scenario(scenario: ArgoverseScenario, scenario_static_map: ArgoverseStaticMap, save_path: Path) -> None: +def visualize_scenario( + scenario: ArgoverseScenario, + scenario_static_map: ArgoverseStaticMap, + save_path: Path, +) -> None: """Build dynamic visualization for all tracks and the local map associated with an Argoverse scenario. Note: This function uses OpenCV to create a MP4 file using the MP4V codec. @@ -69,8 +79,14 @@ def visualize_scenario(scenario: ArgoverseScenario, scenario_static_map: Argover plot_bounds = cur_plot_bounds # Set map bounds to capture focal trajectory history (with fixed buffer in all directions) - plt.xlim(plot_bounds[0] - _PLOT_BOUNDS_BUFFER_M, plot_bounds[1] + _PLOT_BOUNDS_BUFFER_M) - plt.ylim(plot_bounds[2] - _PLOT_BOUNDS_BUFFER_M, plot_bounds[3] + _PLOT_BOUNDS_BUFFER_M) + plt.xlim( + plot_bounds[0] - _PLOT_BOUNDS_BUFFER_M, + plot_bounds[1] + _PLOT_BOUNDS_BUFFER_M, + ) + plt.ylim( + plot_bounds[2] - _PLOT_BOUNDS_BUFFER_M, + plot_bounds[3] + _PLOT_BOUNDS_BUFFER_M, + ) plt.gca().set_aspect("equal", adjustable="box") # Minimize plot margins and make axes invisible @@ -98,7 +114,9 @@ def visualize_scenario(scenario: ArgoverseScenario, scenario_static_map: Argover video.release() -def _plot_static_map_elements(static_map: ArgoverseStaticMap, show_ped_xings: bool = False) -> None: +def _plot_static_map_elements( + static_map: ArgoverseStaticMap, show_ped_xings: bool = False +) -> None: """Plot all static map elements associated with an Argoverse scenario. Args: @@ -123,10 +141,16 @@ def _plot_static_map_elements(static_map: ArgoverseStaticMap, show_ped_xings: bo # Plot pedestrian crossings if show_ped_xings: for ped_xing in static_map.vector_pedestrian_crossings.values(): - _plot_polylines([ped_xing.edge1.xyz, ped_xing.edge2.xyz], alpha=1.0, color=_LANE_SEGMENT_COLOR) + _plot_polylines( + [ped_xing.edge1.xyz, ped_xing.edge2.xyz], + alpha=1.0, + color=_LANE_SEGMENT_COLOR, + ) -def _plot_actor_tracks(ax: plt.Axes, scenario: ArgoverseScenario, timestep: int) -> Optional[_PlotBounds]: +def _plot_actor_tracks( + ax: plt.Axes, scenario: ArgoverseScenario, timestep: int +) -> Optional[_PlotBounds]: """Plot all actor tracks (up to a particular time step) associated with an Argoverse scenario. Args: @@ -141,17 +165,29 @@ def _plot_actor_tracks(ax: plt.Axes, scenario: ArgoverseScenario, timestep: int) for track in scenario.tracks: # Get timesteps for which actor data is valid actor_timesteps: NDArrayInt = np.array( - [object_state.timestep for object_state in track.object_states if object_state.timestep <= timestep] + [ + object_state.timestep + for object_state in track.object_states + if object_state.timestep <= timestep + ] ) if actor_timesteps.shape[0] < 1 or actor_timesteps[-1] != timestep: continue # Get actor trajectory and heading history actor_trajectory: NDArrayFloat = np.array( - [list(object_state.position) for object_state in track.object_states if object_state.timestep <= timestep] + [ + list(object_state.position) + for object_state in track.object_states + if object_state.timestep <= timestep + ] ) actor_headings: NDArrayFloat = np.array( - [object_state.heading for object_state in track.object_states if object_state.timestep <= timestep] + [ + object_state.heading + for object_state in track.object_states + if object_state.timestep <= timestep + ] ) # Plot polyline for focal agent location history @@ -176,7 +212,10 @@ def _plot_actor_tracks(ax: plt.Axes, scenario: ArgoverseScenario, timestep: int) track_color, (_ESTIMATED_VEHICLE_LENGTH_M, _ESTIMATED_VEHICLE_WIDTH_M), ) - elif track.object_type == ObjectType.CYCLIST or track.object_type == ObjectType.MOTORCYCLIST: + elif ( + track.object_type == ObjectType.CYCLIST + or track.object_type == ObjectType.MOTORCYCLIST + ): _plot_actor_bounding_box( ax, actor_trajectory[-1], @@ -185,7 +224,13 @@ def _plot_actor_tracks(ax: plt.Axes, scenario: ArgoverseScenario, timestep: int) (_ESTIMATED_CYCLIST_LENGTH_M, _ESTIMATED_CYCLIST_WIDTH_M), ) else: - plt.plot(actor_trajectory[-1, 0], actor_trajectory[-1, 1], "o", color=track_color, markersize=4) + plt.plot( + actor_trajectory[-1, 0], + actor_trajectory[-1, 1], + "o", + color=track_color, + markersize=4, + ) return track_bounds @@ -208,10 +253,19 @@ def _plot_polylines( color: Desired color for the plotted lines. """ for polyline in polylines: - plt.plot(polyline[:, 0], polyline[:, 1], style, linewidth=line_width, color=color, alpha=alpha) + plt.plot( + polyline[:, 0], + polyline[:, 1], + style, + linewidth=line_width, + color=color, + alpha=alpha, + ) -def _plot_polygons(polygons: Sequence[NDArrayFloat], *, alpha: float = 1.0, color: str = "r") -> None: +def _plot_polygons( + polygons: Sequence[NDArrayFloat], *, alpha: float = 1.0, color: str = "r" +) -> None: """Plot a group of filled polygons with the specified config. Args: @@ -224,7 +278,11 @@ def _plot_polygons(polygons: Sequence[NDArrayFloat], *, alpha: float = 1.0, colo def _plot_actor_bounding_box( - ax: plt.Axes, cur_location: NDArrayFloat, heading: float, color: str, bbox_size: Tuple[float, float] + ax: plt.Axes, + cur_location: NDArrayFloat, + heading: float, + color: str, + bbox_size: Tuple[float, float], ) -> None: """Plot an actor bounding box centered on the actor's current location. @@ -244,6 +302,11 @@ def _plot_actor_bounding_box( pivot_y = cur_location[1] - (d / 2) * math.sin(heading + theta_2) vehicle_bounding_box = Rectangle( - (pivot_x, pivot_y), bbox_length, bbox_width, np.degrees(heading), color=color, zorder=_BOUNDING_BOX_ZORDER + (pivot_x, pivot_y), + bbox_length, + bbox_width, + angle=np.degrees(heading), + color=color, + zorder=_BOUNDING_BOX_ZORDER, ) ax.add_patch(vehicle_bounding_box) diff --git a/src/av2/datasets/sensor/README.md b/src/av2/datasets/sensor/README.md deleted file mode 100644 index f3873de8..00000000 --- a/src/av2/datasets/sensor/README.md +++ /dev/null @@ -1,248 +0,0 @@ -# Argoverse 2 Sensor Dataset Overview - -## Dataset Size - -The Argoverse 2 Sensor Dataset is the successor to the Argoverse 1 3D Tracking Dataset. AV2 is larger, with 1,000 scenes totalling 4.2 hours of driving data, up from 113 scenes in Argoverse 1. - -The total dataset amounts to 1 TB of data in its extracted form. Each vehicle log is approximately 15 seconds in duration and 1 GB in size, including ~150 LiDAR sweeps on average, and ~300 images from each of the 9 cameras (~2700 images per log). - -## Sensor Suite - -Lidar sweeps are collected at 10 Hz, along with 20 fps imagery from 7 ring cameras positioned to provide a fully panoramic field of view, and 20 fps imagery from 2 stereo cameras. In addition, camera intrinsics, extrinsics and 6-DOF ego-vehicle pose in a global coordinate system are provided. Lidar returns are captured by two 32-beam lidars, spinning at 10 Hz in the same direction, but separated in orientation by 180°. The cameras trigger in-sync with both lidars, leading to a 20 Hz frame-rate. The nine global shutter cameras are synchronized to the lidar to have their exposure centered on the lidar sweeping through their fields of view. - -We aggregate all returns from the two stacked 32-beam sensors into a single sweep. These sensors each have different, overlapping fields-of-view. Both lidars have their own reference frame, and we refer to them as `up_lidar` and `down_lidar`, respectively. We have egomotion-compensated the LiDAR sensor data to the egovehicle reference nanosecond timestamp. **All LiDAR returns are provided in the egovehicle reference frame, not the individual LiDAR reference frame**. - -Imagery is provided at (height x width) of `2048 x 1550` (portrait orientation) for the ring front-center camera, and at `1550 x 2048` (landscape orientation) for all other 8 cameras (including the stereo cameras). **All camera imagery is provided in an undistorted format**. - -

- -

- -## Dataset Structure Format - -Tabular data (annotations, lidar sweeps, poses, calibration) are provided as [Apache Feather Files](https://arrow.apache.org/docs/python/feather.html) with the file extension `.feather`. We show examples below. - -## Annotations -Object annotations are provided as 3d cuboids. Their pose is provided in the egovehicle's reference frame. - -```python -io_utils.read_feather("{AV2_ROOT}/01bb304d-7bd8-35f8-bbef-7086b688e35e/annotations.feather") - timestamp_ns track_uuid category length_m width_m height_m qw qx qy qz tx_m ty_m tz_m num_interior_pts -0 315968867659956000 022c398c... BOLLARD 0.363046 0.222484 0.746710 0.68 0.0 0.0 0.72 25.04 -2.55 0.01 10 -1 315968867659956000 12361d61... BOLLARD 0.407004 0.206964 0.792624 0.68 0.0 0.0 0.72 34.13 -2.51 -0.05 5 -2 315968867659956000 12cac1ed... BOLLARD 0.337859 0.227949 0.747096 0.70 0.0 0.0 0.71 21.99 -2.55 0.03 13 -3 315968867659956000 173910b2... BOLLARD 0.326865 0.204709 0.809859 0.71 0.0 0.0 0.69 3.79 -2.53 0.05 16 -4 315968867659956000 23716fb2... BOLLARD 0.336697 0.226178 0.820867 0.72 0.0 0.0 0.69 6.78 -2.52 0.04 19 -... ... ... ... ... ... ... ... ... ... ... ... ... ... ... -18039 315968883159714000 c48fc856... STROLLER 0.581798 0.502284 0.991001 0.97 0.0 0.0 -0.22 -10.84 34.33 0.14 13 -18040 315968883159714000 cf1c4301... TRUCK 9.500000 3.010952 3.573860 -0.51 0.0 0.0 0.85 -26.97 0.09 1.41 1130 -18041 315968883159714000 a834bc72... TRUCK_CAB 9.359874 3.260000 4.949222 0.51 0.0 0.0 0.85 138.13 13.39 0.80 18 -18042 315968883159714000 ff50196f... VEHICULAR_TRAILER 3.414590 2.658412 2.583414 0.84 0.0 0.0 0.52 -13.95 8.32 1.28 533 -18043 315968883159714000 a748a5c4... WHEELED_DEVICE 1.078700 0.479100 1.215600 0.72 0.0 0.0 0.69 19.17 -6.07 0.28 7 -``` -## Pose - -6-DOF ego-vehicle pose in a global (city) coordinate system is provided (visualized in the figure below as a red line, with red circles indicated at a 1 Hz frequency): -

- -

- -We refer to this pose as `city_SE3_egovehicle` throughout the codebase: - -```python ->>> io_utils.read_feather("{AV2_ROOT}/54bc6dbc-ebfb-3fba-b5b3-57f88b4b79ca/city_SE3_egovehicle.feather") - timestamp_ns qw qx qy qz tx_m ty_m tz_m -0 315968112437425433 -0.740565 -0.005635 -0.006869 -0.671926 747.405602 1275.325609 -24.255610 -1 315968112442441182 -0.740385 -0.005626 -0.006911 -0.672124 747.411245 1275.385425 -24.255906 -2 315968112449927216 -0.740167 -0.005545 -0.006873 -0.672365 747.419676 1275.474686 -24.256406 -3 315968112449927217 -0.740167 -0.005545 -0.006873 -0.672365 747.419676 1275.474686 -24.256406 -4 315968112457428271 -0.739890 -0.005492 -0.006953 -0.672669 747.428448 1275.567576 -24.258680 -... ... ... ... ... ... ... ... ... -2692 315968128362451249 -0.694376 -0.001914 -0.006371 -0.719582 740.163738 1467.061503 -24.546971 -2693 315968128372412943 -0.694326 -0.001983 -0.006233 -0.719631 740.160489 1467.147020 -24.545918 -2694 315968128377482496 -0.694346 -0.001896 -0.006104 -0.719613 740.158684 1467.192399 -24.546316 -2695 315968128387425439 -0.694307 -0.001763 -0.005998 -0.719652 740.155543 1467.286735 -24.549918 -2696 315968128392441187 -0.694287 -0.001728 -0.005945 -0.719672 740.153742 1467.331549 -24.550363 - -[2697 rows x 8 columns] -``` - -## LiDAR Sweeps - -For example, we show below the format of an example sweep `sensors/lidar/15970913559644000.feather` (the sweep has a reference timestamp of 15970913559644000 nanoseconds): - -```python - x y z intensity laser_number offset_ns -0 -1.291016 2.992188 -0.229370 24 31 3318000 -1 -25.921875 25.171875 0.992188 5 14 3318000 -2 -15.500000 18.937500 0.901855 34 16 3320303 -3 -3.140625 4.593750 -0.163696 12 30 3320303 -4 -4.445312 6.535156 -0.109802 14 29 3322607 -... ... ... ... ... ... ... -98231 18.312500 -38.187500 3.279297 26 50 106985185 -98232 23.109375 -34.437500 3.003906 20 49 106987490 -98233 4.941406 -5.777344 -0.162720 12 32 106987490 -98234 6.640625 -8.257812 -0.157593 6 33 106989794 -98235 20.015625 -37.062500 2.550781 12 47 106989794 - -[98236 rows x 6 columns] -``` - -## Calibration - -An example calibration file is shown below, parameterizing `vehicle_SE3_sensor` for each sensor (the sensor's pose in the egovehicle coordinate system): - -```python ->>> io_utils.read_feather(f"{AV2_ROOT}/54bc6dbc-ebfb-3fba-b5b3-57f88b4b79ca/calibration/egovehicle_SE3_sensor.feather") - sensor_name qw qx qy qz tx_m ty_m tz_m -0 ring_front_center 0.502809 -0.499689 0.500147 -0.497340 1.631216 -0.000779 1.432780 -1 ring_front_left 0.635526 -0.671957 0.275463 -0.262107 1.550015 0.197539 1.431329 -2 ring_front_right 0.264354 -0.278344 0.671740 -0.633567 1.554057 -0.194171 1.430575 -3 ring_rear_left 0.600598 -0.603227 -0.371096 0.371061 1.104117 0.124369 1.446070 -4 ring_rear_right -0.368149 0.369885 0.603626 -0.602733 1.103432 -0.128317 1.428135 -5 ring_side_left 0.684152 -0.724938 -0.058345 0.054735 1.310427 0.267904 1.433233 -6 ring_side_right -0.053810 0.056105 0.727113 -0.682103 1.310236 -0.273345 1.435529 -7 stereo_front_left 0.500421 -0.499934 0.501241 -0.498399 1.625085 0.248148 1.222831 -8 stereo_front_right 0.500885 -0.503584 0.498793 -0.496713 1.633076 -0.250872 1.222173 -9 up_lidar 0.999996 0.000000 0.000000 -0.002848 1.350180 0.000000 1.640420 -10 down_lidar -0.000089 -0.994497 0.104767 0.000243 1.355162 0.000133 1.565252 -``` - -## Intrinsics - -An example camera intrinsics file is shown below: - -```python ->>> io_utils.read_feather("{AV2_ROOT}/54bc6dbc-ebfb-3fba-b5b3-57f88b4b79ca/calibration/intrinsics.feather") - sensor_name fx_px fy_px cx_px ... k2 k3 height_px width_px -0 ring_front_center 1773.504272 1773.504272 775.826693 ... -0.212167 0.328694 2048 1550 -1 ring_front_left 1682.010713 1682.010713 1025.068254 ... -0.136984 0.209330 1550 2048 -2 ring_front_right 1684.834479 1684.834479 1024.373455 ... -0.133341 0.208709 1550 2048 -3 ring_rear_left 1686.494558 1686.494558 1025.655905 ... -0.129761 0.202326 1550 2048 -4 ring_rear_right 1683.375120 1683.375120 1024.381124 ... -0.129331 0.201599 1550 2048 -5 ring_side_left 1684.902403 1684.902403 1027.822264 ... -0.124561 0.196519 1550 2048 -6 ring_side_right 1682.936559 1682.936559 1024.948976 ... -0.109515 0.179383 1550 2048 -7 stereo_front_left 1685.825885 1685.825885 1025.830335 ... -0.113065 0.182441 1550 2048 -8 stereo_front_right 1683.137591 1683.137591 1024.612074 ... -0.127301 0.198538 1550 2048 -``` - -A local map is provided per log, please refer to the [Map README](../../map/README.md) for additional details. - -## Log Distribution Across Cities -Vehicle logs from the **AV2 Sensor Dataset** are captured in 6 cities, according to the following distribution: -- Austin, Texas: 31 logs. -- Detroit, Michigan: 117 logs. -- Miami, Florida: 354 logs. -- Pittsburgh, Pennsylvania: 350 logs. -- Palo Alto, California: 22 logs. -- Washington, D.C.: 126 logs. - -## Privacy - -All faces and license plates, whether inside vehicles or outside of the drivable area, are blurred extensively to preserve privacy. - -## Sensor Dataset splits - -We randomly partitioned 1000 logs into the following splits: - -- Train (700 logs) -- Validation (150 logs) -- Test (150 logs) - -## Sensor Dataset Taxonomy - -The AV2 Sensor Dataset contains 10 Hz 3D cuboid annotations for objects within our 30 class taxonomy. Objects are annotated if they are within the “region of interest” (ROI) – within five meters of the mapped “driveable” area. - -![cuboids-by-category](https://user-images.githubusercontent.com/29715011/158675968-3987b2bc-ed8b-4194-85ca-44298805cfb2.png) - -These 30 classes are defined as follows, appearing in order of frequency: - -1. `REGULAR_VEHICLE`: -Any conventionally sized passenger vehicle used for the transportation of people and cargo. This includes Cars, vans, pickup trucks, SUVs, etc. - -2. ``PEDESTRIAN``: -Person that is not driving or riding in/on a vehicle. They can be walking, standing, sitting, prone, etc. - -3. `BOLLARD`: -Bollards are short, sturdy posts installed in the roadway or sidewalk to control the flow of traffic. These may be temporary or permanent and are sometimes decorative. - -4. `CONSTRUCTION_CONE`: -Movable traffic cone that is used to alert drivers to a hazard. These will typically be orange and white striped and may or may not have a blinking light attached to the top. - -5. `CONSTRUCTION_BARREL`: -Construction Barrel is a movable traffic barrel that is used to alert drivers to a hazard. These will typically be orange and white striped and may or may not have a blinking light attached to the top. - -6. `STOP_SIGN`: -Red octagonal traffic sign displaying the word STOP used to notify drivers that they must come to a complete stop and make sure no other road users are coming before proceeding. - -7. `BICYCLE`: -Non-motorized vehicle that typically has two wheels and is propelled by human power pushing pedals in a circular motion. - -8. `LARGE_VEHICLE`: -Large motorized vehicles (four wheels or more) which do not fit into any more specific subclass. Examples include extended passenger vans, fire trucks, RVs, etc. - -9. `WHEELED_DEVICE`: -Objects involved in the transportation of a person and do not fit a more specific class. Examples range from skateboards, non-motorized scooters, segways, to golf-carts. - -10. `BUS`: -Standard city buses designed to carry a large number of people. - -11. `BOX_TRUCK`: -Chassis cab truck with an enclosed cube shaped cargo area. It should be noted that the cargo area is rigidly attached to the cab, and they do not articulate. - -12. `SIGN`: -Official road signs placed by the Department of Transportation (DOT signs) which are of interest to us. This includes yield signs, speed limit signs, directional control signs, construction signs, and other signs that provide required traffic control information. Note that Stop Sign is captured separately and informative signs such as street signs, parking signs, bus stop signs, etc. are not included in this class. - -13. `TRUCK`: -Vehicles that are clearly defined as a truck but does not fit into the subclasses of Box Truck or Truck Cab. Examples include common delivery vehicles (UPS, FedEx), mail trucks, garbage trucks, utility trucks, ambulances, dump trucks, etc. - -14. `MOTORCYCLE`: -Motorized vehicle with two wheels where the rider straddles the engine. These are capable of high speeds similar to a car. - -15. `BICYCLIST`: -Person actively riding a bicycle, non-pedaling passengers included. - -16. `VEHICULAR_TRAILER`: -Non-motorized, wheeled vehicle towed behind a motorized vehicle. - -17. `TRUCK_CAB`: -Heavy truck commonly known as “Semi cab”, “Tractor”, or “Lorry”. This refers to only the front of part of an articulated tractor trailer. - -18. `MOTORCYCLIST`: -Person actively riding a motorcycle or a moped, including passengers. - -19. `DOG`: -Any member of the canine family. - -20. `SCHOOL_BUS`: -Bus that primarily holds school children (typically yellow) and can control the flow of traffic via the use of an articulating stop sign and loading/unloading flasher lights. - -21. `WHEELED_RIDER`: -Person actively riding or being carried by a wheeled device. - -22. `STROLLER`: -Push-cart with wheels meant to hold a baby or toddler. - -23. `ARTICULATED_BUS`: -Articulated buses perform the same function as a standard city bus, but are able to bend (articulate) towards the center. These will also have a third set of wheels not present on a typical bus. - -24. `MESSAGE_BOARD_TRAILER`: -Trailer carrying a large, mounted, electronic sign to display messages. Often found around construction sites or large events. - -25. `MOBILE_PEDESTRIAN_SIGN`: -Movable sign designating an area where pedestrians may cross the road. - -26. `WHEELCHAIR`: -Chair fitted with wheels for use as a means of transport by a person who is unable to walk as a result of illness, injury, or disability. This includes both motorized and non-motorized wheelchairs as well as low-speed seated scooters not intended for use on the roadway. - -27. `RAILED_VEHICLE`: -Any vehicle that relies on rails to move. This applies to trains, trolleys, train engines, train freight cars, train tanker cars, subways, etc. - -28. `OFFICIAL_SIGNALER`: -Person with authority specifically responsible for stopping and directing vehicles through traffic. - -29. `TRAFFIC_LIGHT_TRAILER`: -Mounted, portable traffic light unit commonly used in construction zones or for other temporary detours. - -30. `ANIMAL`: -All recognized animals large enough to affect traffic, but that do not fit into the Cat, Dog, or Horse categories diff --git a/src/av2/datasets/sensor/__init__.py b/src/av2/datasets/sensor/__init__.py index 8986d95b..4ab690f9 100644 --- a/src/av2/datasets/sensor/__init__.py +++ b/src/av2/datasets/sensor/__init__.py @@ -1,3 +1,3 @@ # -"""Sensor dataset submodule.""" +"""Sensor dataset sub-package.""" diff --git a/src/av2/datasets/sensor/av2_sensor_dataloader.py b/src/av2/datasets/sensor/av2_sensor_dataloader.py index 7204e36a..03625b25 100644 --- a/src/av2/datasets/sensor/av2_sensor_dataloader.py +++ b/src/av2/datasets/sensor/av2_sensor_dataloader.py @@ -1,5 +1,6 @@ # -"""Implements a dataloader for the Argoverse 2.0 Sensor and TbV Datasets.""" + +"""Implements a dataloader for the Argoverse 2 Sensor and TbV Datasets.""" import logging from pathlib import Path @@ -33,14 +34,16 @@ def __init__(self, data_dir: Path, labels_dir: Path) -> None: """Create the Sensor dataloader from a data directory and labels directory. Args: - data_dir: Path to raw Argoverse 2.0 data - labels_dir: Path to Argoverse 2.0 data labels (e.g. labels or estimated detections/tracks) + data_dir: Path to raw Argoverse 2 data + labels_dir: Path to Argoverse 2 data labels (e.g. labels or estimated detections/tracks) Raises: ValueError: if input arguments are not Path objects. """ if not isinstance(data_dir, Path) or not isinstance(labels_dir, Path): - raise ValueError("Input arguments must be Path objects, representing paths to local directories") + raise ValueError( + "Input arguments must be Path objects, representing paths to local directories" + ) self._data_dir = data_dir self._labels_dir = labels_dir @@ -66,7 +69,9 @@ def get_city_SE3_ego(self, log_id: str, timestamp_ns: int) -> SE3: Raises: RuntimeError: If no recorded pose is available for the requested timestamp. """ - log_poses_df = io_utils.read_feather(self._data_dir / log_id / "city_SE3_egovehicle.feather") + log_poses_df = io_utils.read_feather( + self._data_dir / log_id / "city_SE3_egovehicle.feather" + ) pose_df = log_poses_df.loc[log_poses_df["timestamp_ns"] == timestamp_ns] if len(pose_df) == 0: @@ -75,7 +80,9 @@ def get_city_SE3_ego(self, log_id: str, timestamp_ns: int) -> SE3: city_SE3_ego = convert_pose_dataframe_to_SE3(pose_df) return city_SE3_ego - def get_subsampled_ego_trajectory(self, log_id: str, sample_rate_hz: float = 1.0) -> NDArrayFloat: + def get_subsampled_ego_trajectory( + self, log_id: str, sample_rate_hz: float = 1.0 + ) -> NDArrayFloat: """Get the trajectory of the AV (egovehicle) at an approximate sampling rate (Hz). Note: the trajectory is NOT interpolated to give an *exact* sampling rate. @@ -100,7 +107,9 @@ def get_subsampled_ego_trajectory(self, log_id: str, sample_rate_hz: float = 1.0 MAX_MEASUREMENT_FREQUENCY_HZ, ) - log_poses_df = io_utils.read_feather(self._data_dir / log_id / "city_SE3_egovehicle.feather") + log_poses_df = io_utils.read_feather( + self._data_dir / log_id / "city_SE3_egovehicle.feather" + ) # timestamp of the pose measurement. timestamp_ns = list(log_poses_df.timestamp_ns) @@ -158,7 +167,9 @@ def get_log_ids(self) -> List[str]: """Return a list of all vehicle log IDs available at the provided dataroot.""" return sorted([d.name for d in self._data_dir.glob("*") if d.is_dir()]) - def get_closest_img_fpath(self, log_id: str, cam_name: str, lidar_timestamp_ns: int) -> Optional[Path]: + def get_closest_img_fpath( + self, log_id: str, cam_name: str, lidar_timestamp_ns: int + ) -> Optional[Path]: """Return the filepath corresponding to the closest image from the lidar timestamp. Args: @@ -169,13 +180,24 @@ def get_closest_img_fpath(self, log_id: str, cam_name: str, lidar_timestamp_ns: Returns: im_fpath: path to image if one is found within the expected time interval, or else None. """ - cam_timestamp_ns = self._sdb.get_closest_cam_channel_timestamp(lidar_timestamp_ns, cam_name, log_id) + cam_timestamp_ns = self._sdb.get_closest_cam_channel_timestamp( + lidar_timestamp_ns, cam_name, log_id + ) if cam_timestamp_ns is None: return None - img_fpath = self._data_dir / log_id / "sensors" / "cameras" / cam_name / f"{cam_timestamp_ns}.jpg" + img_fpath = ( + self._data_dir + / log_id + / "sensors" + / "cameras" + / cam_name + / f"{cam_timestamp_ns}.jpg" + ) return img_fpath - def get_closest_lidar_fpath(self, log_id: str, cam_timestamp_ns: int) -> Optional[Path]: + def get_closest_lidar_fpath( + self, log_id: str, cam_timestamp_ns: int + ) -> Optional[Path]: """Get file path for LiDAR sweep accumulated to a timestamp closest to a camera timestamp. Args: @@ -185,14 +207,18 @@ def get_closest_lidar_fpath(self, log_id: str, cam_timestamp_ns: int) -> Optiona Returns: lidar_fpath: path to sweep .feather file if one is found within the expected time interval, or else None. """ - lidar_timestamp_ns = self._sdb.get_closest_lidar_timestamp(cam_timestamp_ns, log_id) + lidar_timestamp_ns = self._sdb.get_closest_lidar_timestamp( + cam_timestamp_ns, log_id + ) if lidar_timestamp_ns is None: return None lidar_fname = f"{lidar_timestamp_ns}.feather" lidar_fpath = self._data_dir / log_id / "sensors" / "lidar" / lidar_fname return lidar_fpath - def get_lidar_fpath_at_lidar_timestamp(self, log_id: str, lidar_timestamp_ns: int) -> Optional[Path]: + def get_lidar_fpath_at_lidar_timestamp( + self, log_id: str, lidar_timestamp_ns: int + ) -> Optional[Path]: """Return the file path for the LiDAR sweep accumulated to the query timestamp, if it exists. Args: @@ -232,7 +258,9 @@ def get_ordered_log_lidar_timestamps(self, log_id: str) -> List[int]: Returns: lidar_timestamps_ns: ordered timestamps, provided in nanoseconds. """ - ordered_lidar_fpaths: List[Path] = self.get_ordered_log_lidar_fpaths(log_id=log_id) + ordered_lidar_fpaths: List[Path] = self.get_ordered_log_lidar_fpaths( + log_id=log_id + ) lidar_timestamps_ns = [int(fp.stem) for fp in ordered_lidar_fpaths] return lidar_timestamps_ns @@ -260,10 +288,14 @@ def get_ordered_log_cam_fpaths(self, log_id: str, cam_name: str) -> List[Path]: Returns: List of paths, representing paths to ordered JPEG files in this log, for a specific camera. """ - cam_img_fpaths = sorted(self._data_dir.glob(f"{log_id}/sensors/cameras/{cam_name}/*.jpg")) + cam_img_fpaths = sorted( + self._data_dir.glob(f"{log_id}/sensors/cameras/{cam_name}/*.jpg") + ) return cam_img_fpaths - def get_labels_at_lidar_timestamp(self, log_id: str, lidar_timestamp_ns: int) -> CuboidList: + def get_labels_at_lidar_timestamp( + self, log_id: str, lidar_timestamp_ns: int + ) -> CuboidList: """Load the sweep annotations at the provided timestamp. Args: @@ -279,7 +311,9 @@ def get_labels_at_lidar_timestamp(self, log_id: str, lidar_timestamp_ns: int) -> # NOTE: This file contains annotations for the ENTIRE sequence. # The sweep annotations are selected below. cuboid_list = CuboidList.from_feather(annotations_feather_path) - cuboids = list(filter(lambda x: x.timestamp_ns == lidar_timestamp_ns, cuboid_list.cuboids)) + cuboids = list( + filter(lambda x: x.timestamp_ns == lidar_timestamp_ns, cuboid_list.cuboids) + ) return CuboidList(cuboids=cuboids) def project_ego_to_img_motion_compensated( @@ -309,11 +343,15 @@ def project_ego_to_img_motion_compensated( # get transformation to bring point in egovehicle frame to city frame, # at the time when camera image was recorded. - city_SE3_ego_cam_t = self.get_city_SE3_ego(log_id=log_id, timestamp_ns=cam_timestamp_ns) + city_SE3_ego_cam_t = self.get_city_SE3_ego( + log_id=log_id, timestamp_ns=cam_timestamp_ns + ) # get transformation to bring point in egovehicle frame to city frame, # at the time when the LiDAR sweep was recorded. - city_SE3_ego_lidar_t = self.get_city_SE3_ego(log_id=log_id, timestamp_ns=lidar_timestamp_ns) + city_SE3_ego_lidar_t = self.get_city_SE3_ego( + log_id=log_id, timestamp_ns=lidar_timestamp_ns + ) return pinhole_camera.project_ego_to_img_motion_compensated( points_lidar_time=points_lidar_time, @@ -336,9 +374,13 @@ def get_colored_sweep(self, log_id: str, lidar_timestamp_ns: int) -> NDArrayByte Raises: ValueError: If requested timestamp has no corresponding LiDAR sweep. """ - lidar_fpath = self.get_lidar_fpath_at_lidar_timestamp(log_id=log_id, lidar_timestamp_ns=lidar_timestamp_ns) + lidar_fpath = self.get_lidar_fpath_at_lidar_timestamp( + log_id=log_id, lidar_timestamp_ns=lidar_timestamp_ns + ) if lidar_fpath is None: - raise ValueError("Requested colored sweep at a timestamp that has no corresponding LiDAR sweep.") + raise ValueError( + "Requested colored sweep at a timestamp that has no corresponding LiDAR sweep." + ) sweep = Sweep.from_feather(lidar_fpath) n_sweep_pts = len(sweep) @@ -362,7 +404,7 @@ def get_colored_sweep(self, log_id: str, lidar_timestamp_ns: int) -> NDArrayByte lidar_timestamp_ns=lidar_timestamp_ns, log_id=log_id, ) - uv_valid = np.round(uv[is_valid]).astype(np.int64) # type: ignore + uv_valid = np.round(uv[is_valid]).astype(np.int64) u = uv_valid[:, 0] v = uv_valid[:, 1] img = io_utils.read_img(img_fpath, channel_order="RGB") @@ -412,8 +454,8 @@ def get_depth_map_from_lidar( if is_valid_points is None or is_valid_points.sum() == 0: return None - u = np.round(uv[:, 0][is_valid_points]).astype(np.int32) # type: ignore - v = np.round(uv[:, 1][is_valid_points]).astype(np.int32) # type: ignore + u = np.round(uv[:, 0][is_valid_points]).astype(np.int32) + v = np.round(uv[:, 1][is_valid_points]).astype(np.int32) z = points_cam[:, 2][is_valid_points] depth_map: NDArrayFloat = np.zeros((height_px, width_px), dtype=np.float32) @@ -421,7 +463,9 @@ def get_depth_map_from_lidar( # form depth map from LiDAR if interp_depth_map: if u.max() > pinhole_camera.width_px or v.max() > pinhole_camera.height_px: - raise RuntimeError("Regular grid interpolation will fail due to out-of-bound inputs.") + raise RuntimeError( + "Regular grid interpolation will fail due to out-of-bound inputs." + ) depth_map = dense_grid_interpolation.interp_dense_grid_from_sparse( grid_img=depth_map, diff --git a/src/av2/datasets/sensor/sensor_dataloader.py b/src/av2/datasets/sensor/sensor_dataloader.py index aaa7ccbf..2da81108 100644 --- a/src/av2/datasets/sensor/sensor_dataloader.py +++ b/src/av2/datasets/sensor/sensor_dataloader.py @@ -22,7 +22,12 @@ from av2.structures.sweep import Sweep from av2.structures.timestamped_image import TimestampedImage from av2.utils.constants import HOME -from av2.utils.io import TimestampedCitySE3EgoPoses, read_city_SE3_ego, read_feather, read_img +from av2.utils.io import ( + TimestampedCitySE3EgoPoses, + read_city_SE3_ego, + read_feather, + read_img, +) from av2.utils.metric_time import TimeUnit, to_metric_time logger = logging.Logger(__name__) @@ -40,13 +45,19 @@ # constants defined in milliseconds # below evaluates to 50 ms -CAM_SHUTTER_INTERVAL_MS: Final[float] = to_metric_time(ts=1 / CAM_FPS, src=Second, dst=Millisecond) +CAM_SHUTTER_INTERVAL_MS: Final[float] = to_metric_time( + ts=1 / CAM_FPS, src=Second, dst=Millisecond +) # below evaluates to 100 ms -LIDAR_SWEEP_INTERVAL_MS: Final[float] = to_metric_time(ts=1 / LIDAR_FRAME_RATE_HZ, src=Second, dst=Millisecond) +LIDAR_SWEEP_INTERVAL_MS: Final[float] = to_metric_time( + ts=1 / LIDAR_FRAME_RATE_HZ, src=Second, dst=Millisecond +) ALLOWED_TIMESTAMP_BUFFER_MS: Final[int] = 2 # allow 2 ms of buffer -LIDAR_SWEEP_INTERVAL_W_BUFFER_MS: Final[float] = LIDAR_SWEEP_INTERVAL_MS + ALLOWED_TIMESTAMP_BUFFER_MS +LIDAR_SWEEP_INTERVAL_W_BUFFER_MS: Final[float] = ( + LIDAR_SWEEP_INTERVAL_MS + ALLOWED_TIMESTAMP_BUFFER_MS +) LIDAR_SWEEP_INTERVAL_W_BUFFER_NS: Final[float] = to_metric_time( ts=LIDAR_SWEEP_INTERVAL_W_BUFFER_MS, src=Millisecond, dst=Nanosecond ) @@ -112,7 +123,7 @@ def __post_init__(self) -> None: """Index the dataset for fast sensor data lookup. Synchronization database and sensor records are separate tables. Sensor records are an enumeration of - the records. The synchronization database is a hierarchichal index (Pandas MultiIndex) that functions + the records. The synchronization database is a hierarchical index (Pandas MultiIndex) that functions as a lookup table with correspondences between nearest images. Given reference LiDAR timestamp -> obtain 7 closest ring camera + 2 stereo camera timestamps. First level: Log id (1000 uuids) @@ -151,7 +162,9 @@ def __post_init__(self) -> None: # Populate synchronization database. if self.cam_names: - synchronization_cache_path = HOME / ".cache" / "av2" / "synchronization_cache.feather" + synchronization_cache_path = ( + HOME / ".cache" / "av2" / "synchronization_cache.feather" + ) synchronization_cache_path.parent.mkdir(parents=True, exist_ok=True) # If caching is enabled AND the path exists, then load from the cache file. @@ -165,7 +178,9 @@ def __post_init__(self) -> None: self.synchronization_cache.to_feather(str(synchronization_cache_path)) # Finally, create a MultiIndex set the sync records index and sort it. - self.synchronization_cache.set_index(keys=["split", "log_id", "sensor_name"], inplace=True) + self.synchronization_cache.set_index( + keys=["split", "log_id", "sensor_name"], inplace=True + ) self.synchronization_cache.sort_index(inplace=True) @cached_property @@ -181,7 +196,9 @@ def num_sweeps(self) -> int: @cached_property def sensor_counts(self) -> pd.Series: """Return the number of records for each sensor.""" - sensor_counts: pd.Series = self.sensor_cache.index.get_level_values("sensor_name").value_counts() + sensor_counts: pd.Series = self.sensor_cache.index.get_level_values( + "sensor_name" + ).value_counts() return sensor_counts @property @@ -212,6 +229,8 @@ def _build_sensor_cache(self) -> pd.DataFrame: if self.with_cache and sensor_cache_path.exists(): logger.info("Cache found. Loading from disk ...") sensor_cache = read_feather(sensor_cache_path) + if self.with_annotations: + sensor_cache = sensor_cache[sensor_cache.split != "test"] else: lidar_records = self.populate_lidar_records() # Load camera records if enabled. @@ -229,7 +248,9 @@ def _build_sensor_cache(self) -> pd.DataFrame: # Set index as tuples of the form: (split, log_id, sensor_name, timestamp_ns) and sort the index. # sorts by split, log_id, and then by sensor name, and then by timestamp. - sensor_cache.set_index(["split", "log_id", "sensor_name", "timestamp_ns"], inplace=True) + sensor_cache.set_index( + ["split", "log_id", "sensor_name", "timestamp_ns"], inplace=True + ) sensor_cache.sort_index(inplace=True) # Return all of the sensor records. @@ -243,9 +264,12 @@ def populate_lidar_records(self) -> pd.DataFrame: N is the number of sweeps for all logs in the dataset, and the `sensor_name` column should be populated with `lidar` in every entry. """ - lidar_paths = sorted(self.dataset_dir.glob(LIDAR_PATTERN), key=lambda x: int(x.stem)) + lidar_paths = sorted( + self.dataset_dir.glob(LIDAR_PATTERN), key=lambda x: int(x.stem) + ) lidar_record_list = [ - convert_path_to_named_record(x) for x in track(lidar_paths, description="Loading lidar records ...") + convert_path_to_named_record(x) + for x in track(lidar_paths, description="Loading lidar records ...") ] # Concatenate into single dataframe (list-of-dicts to DataFrame). @@ -262,11 +286,14 @@ def populate_image_records(self) -> pd.DataFrame: every entry. """ # Get sorted list of camera paths. - cam_paths = sorted(self.dataset_dir.glob(CAMERA_PATTERN), key=lambda x: int(x.stem)) + cam_paths = sorted( + self.dataset_dir.glob(CAMERA_PATTERN), key=lambda x: int(x.stem) + ) # Load entire set of camera records. cam_record_list = [ - convert_path_to_named_record(x) for x in track(cam_paths, description="Loading camera records ...") + convert_path_to_named_record(x) + for x in track(cam_paths, description="Loading camera records ...") ] # Concatenate into single dataframe (list-of-dicts to DataFrame). @@ -308,7 +335,9 @@ def __getitem__(self, idx: int) -> SynchronizedSensorData: """ # Grab the lidar record at the specified index. # Selects data at a particular level of a MultiIndex. - record: Tuple[str, str, int] = self.sensor_cache.xs(key="lidar", level=2).iloc[idx].name + record: Tuple[str, str, int] = ( + self.sensor_cache.xs(key="lidar", level=2).iloc[idx].name + ) # Grab the identifying record fields. split, log_id, timestamp_ns = record @@ -338,11 +367,14 @@ def __getitem__(self, idx: int) -> SynchronizedSensorData: # Load annotations if enabled. if self.with_annotations: - datum.annotations = self._load_annotations(split, log_id, timestamp_ns) + if split != "test": + datum.annotations = self._load_annotations(split, log_id, timestamp_ns) # Load camera imagery if enabled. if self.cam_names: - datum.synchronized_imagery = self._load_synchronized_cams(split, sensor_dir, log_id, timestamp_ns) + datum.synchronized_imagery = self._load_synchronized_cams( + split, sensor_dir, log_id, timestamp_ns + ) # Return datum at the specified index. return datum @@ -365,31 +397,41 @@ def _build_synchronization_cache(self) -> pd.DataFrame: # Create list to store synchronized data frames. sync_list: List[pd.DataFrame] = [] - unique_sensor_names: List[str] = self.sensor_cache.index.unique(level=2).tolist() + unique_sensor_names: List[str] = self.sensor_cache.index.unique( + level=2 + ).tolist() # Associate a 'source' sensor to a 'target' sensor for all available sensors. # For example, we associate the lidar sensor with each ring camera which # produces a mapping from lidar -> all-other-sensors. for src_sensor_name in unique_sensor_names: - src_records = self.sensor_cache.xs(src_sensor_name, level=2, drop_level=False).reset_index() - src_records = src_records.rename({"timestamp_ns": src_sensor_name}, axis=1).sort_values(src_sensor_name) + src_records = self.sensor_cache.xs( + src_sensor_name, level=2, drop_level=False + ).reset_index() + src_records = src_records.rename( + {"timestamp_ns": src_sensor_name}, axis=1 + ).sort_values(src_sensor_name) # _Very_ important to convert to timedelta. Tolerance below causes precision loss otherwise. src_records[src_sensor_name] = pd.to_timedelta(src_records[src_sensor_name]) for target_sensor_name in unique_sensor_names: if src_sensor_name == target_sensor_name: continue - target_records = self.sensor_cache.xs(target_sensor_name, level=2).reset_index() - target_records = target_records.rename({"timestamp_ns": target_sensor_name}, axis=1).sort_values( - target_sensor_name - ) + target_records = self.sensor_cache.xs( + target_sensor_name, level=2 + ).reset_index() + target_records = target_records.rename( + {"timestamp_ns": target_sensor_name}, axis=1 + ).sort_values(target_sensor_name) # Merge based on matching criterion. # _Very_ important to convert to timedelta. Tolerance below causes precision loss otherwise. - target_records[target_sensor_name] = pd.to_timedelta(target_records[target_sensor_name]) - tolerence = pd.to_timedelta(CAM_SHUTTER_INTERVAL_MS / 2 * 1e6) + target_records[target_sensor_name] = pd.to_timedelta( + target_records[target_sensor_name] + ) + tolerance = pd.to_timedelta(CAM_SHUTTER_INTERVAL_MS / 2 * 1e6) if "ring" in src_sensor_name: - tolerence = pd.to_timedelta(LIDAR_SWEEP_INTERVAL_W_BUFFER_NS / 2) + tolerance = pd.to_timedelta(LIDAR_SWEEP_INTERVAL_W_BUFFER_NS / 2) src_records = pd.merge_asof( src_records, target_records, @@ -397,7 +439,7 @@ def _build_synchronization_cache(self) -> pd.DataFrame: right_on=target_sensor_name, by=["split", "log_id"], direction=self.matching_criterion, - tolerance=tolerence, + tolerance=tolerance, ) sync_list.append(src_records) sync_records = pd.concat(sync_list).reset_index(drop=True) @@ -427,33 +469,45 @@ def find_closest_target_fpath( RuntimeError: if the synchronization database (sync_records) has not been created. """ if self.synchronization_cache is None: - raise RuntimeError("Requested synchronized data, but the synchronization database has not been created.") + raise RuntimeError( + "Requested synchronized data, but the synchronization database has not been created." + ) src_timedelta_ns = pd.Timedelta(src_timestamp_ns) - src_to_target_records = self.synchronization_cache.loc[(split, log_id, src_sensor_name)].set_index( - src_sensor_name - ) + src_to_target_records = self.synchronization_cache.loc[ + (split, log_id, src_sensor_name) + ].set_index(src_sensor_name) index = src_to_target_records.index if src_timedelta_ns not in index: # This timestamp does not correspond to any lidar sweep. return None # Grab the synchronization record. - target_timestamp_ns = src_to_target_records.loc[src_timedelta_ns, target_sensor_name] + target_timestamp_ns = src_to_target_records.loc[ + src_timedelta_ns, target_sensor_name + ] if pd.isna(target_timestamp_ns): # No match was found within tolerance. return None sensor_dir = self.dataset_dir / split / log_id / "sensors" - valid_cameras = [x.value for x in list(RingCameras)] + [x.value for x in list(StereoCameras)] + valid_cameras = [x.value for x in list(RingCameras)] + [ + x.value for x in list(StereoCameras) + ] timestamp_ns_str = str(target_timestamp_ns.asm8.item()) if target_sensor_name in valid_cameras: - target_path = sensor_dir / "cameras" / target_sensor_name / f"{timestamp_ns_str}.jpg" + target_path = ( + sensor_dir / "cameras" / target_sensor_name / f"{timestamp_ns_str}.jpg" + ) else: - target_path = sensor_dir / target_sensor_name / f"{timestamp_ns_str}.feather" + target_path = ( + sensor_dir / target_sensor_name / f"{timestamp_ns_str}.feather" + ) return target_path - def get_closest_img_fpath(self, split: str, log_id: str, cam_name: str, lidar_timestamp_ns: int) -> Optional[Path]: + def get_closest_img_fpath( + self, split: str, log_id: str, cam_name: str, lidar_timestamp_ns: int + ) -> Optional[Path]: """Find the file path to the closest image from the reference camera name to the lidar timestamp. Args: @@ -473,7 +527,9 @@ def get_closest_img_fpath(self, split: str, log_id: str, cam_name: str, lidar_ti target_sensor_name=cam_name, ) - def get_closest_lidar_fpath(self, split: str, log_id: str, cam_name: str, cam_timestamp_ns: int) -> Optional[Path]: + def get_closest_lidar_fpath( + self, split: str, log_id: str, cam_name: str, cam_timestamp_ns: int + ) -> Optional[Path]: """Find the file path to the closest image from the lidar to the reference camera. Args: @@ -493,7 +549,9 @@ def get_closest_lidar_fpath(self, split: str, log_id: str, cam_name: str, cam_ti target_sensor_name="lidar", ) - def _load_annotations(self, split: str, log_id: str, sweep_timestamp_ns: int) -> CuboidList: + def _load_annotations( + self, split: str, log_id: str, sweep_timestamp_ns: int + ) -> CuboidList: """Load the sweep annotations at the provided timestamp. Args: @@ -504,13 +562,17 @@ def _load_annotations(self, split: str, log_id: str, sweep_timestamp_ns: int) -> Returns: Cuboid list of annotations. """ - annotations_feather_path = self.dataset_dir / split / log_id / "annotations.feather" + annotations_feather_path = ( + self.dataset_dir / split / log_id / "annotations.feather" + ) # Load annotations from disk. # NOTE: This contains annotations for the ENTIRE sequence. # The sweep annotations are selected below. cuboid_list = CuboidList.from_feather(annotations_feather_path) - cuboids = list(filter(lambda x: x.timestamp_ns == sweep_timestamp_ns, cuboid_list.cuboids)) + cuboids = list( + filter(lambda x: x.timestamp_ns == sweep_timestamp_ns, cuboid_list.cuboids) + ) return CuboidList(cuboids=cuboids) def _load_synchronized_cams( @@ -531,7 +593,9 @@ def _load_synchronized_cams( RuntimeError: if the synchronization database (sync_records) has not been created. """ if self.synchronization_cache is None: - raise RuntimeError("Requested synchronized data, but the synchronization database has not been created.") + raise RuntimeError( + "Requested synchronized data, but the synchronization database has not been created." + ) cam_paths = [ self.find_closest_target_fpath( @@ -550,7 +614,9 @@ def _load_synchronized_cams( if p is not None: cams[p.parent.stem] = TimestampedImage( img=read_img(p, channel_order="BGR"), - camera_model=PinholeCamera.from_feather(log_dir=log_dir, cam_name=p.parent.stem), + camera_model=PinholeCamera.from_feather( + log_dir=log_dir, cam_name=p.parent.stem + ), timestamp_ns=int(p.stem), ) return cams diff --git a/src/av2/datasets/sensor/utils.py b/src/av2/datasets/sensor/utils.py index 2996c473..9e5c5350 100644 --- a/src/av2/datasets/sensor/utils.py +++ b/src/av2/datasets/sensor/utils.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Dict, Union -logger = logging.Logger(__name__) +logger = logging.getLogger(__name__) def convert_path_to_named_record(path: Path) -> Dict[str, Union[str, int]]: @@ -20,7 +20,11 @@ def convert_path_to_named_record(path: Path) -> Dict[str, Union[str, int]]: """ sensor_path = path.parent sensor_name = sensor_path.stem - log_path = sensor_path.parent.parent if sensor_name == "lidar" else sensor_path.parent.parent.parent + log_path = ( + sensor_path.parent.parent + if sensor_name == "lidar" + else sensor_path.parent.parent.parent + ) # log_id is 2 directories up for the lidar filepaths, but 3 levels up for images # {log_id}/sensors/cameras/ring_*/*.jpg vs. diff --git a/src/av2/datasets/tbv/README.md b/src/av2/datasets/tbv/README.md deleted file mode 100644 index 71cbb989..00000000 --- a/src/av2/datasets/tbv/README.md +++ /dev/null @@ -1,174 +0,0 @@ -# Trust, but Verify (TbV) Dataset Overview - -
- - - - -
- -## Dataset Size - -The Trust, but Verify (TbV) Dataset consists of 1043 vehicle logs. Each vehicle log, on average, is 54 seconds in duration, including 536 LiDAR sweeps on average, and 1073 images from each of the 7 cameras (7512 images per log). Some logs are as short as 4 seconds, and other logs are up to 117 seconds in duration. - -The total dataset amounts to 15.54 hours of driving data, amounting to 922 GB of data in its extracted form. There are 7.84 Million images in the dataset (7,837,614 exactly), and 559,440 LiDAR sweeps in total. - -## Downloading TbV - -TbV is available for download in two forms -- either zipped up as 21 tar.gz files -- or in extracted, unzipped form (without tar archives). Downloading either will produce the same result (the underlying log data is identical). - -Using the `tar.gz` files is recommended (depending upon your connection, this is likely faster, as there are almost 8 million images files in the extracted format). We recommend using `s5cmd` to pull down all 21 `.tar.gz` files with a single command. You can see the links to the `tar.gz` files on [the Argoverse 2 downloads page](https://www.argoverse.org/av2.html#download-link). - -First, install `s5cmd` using [the installation instructions here](https://github.com/argoai/argoverse2-api/blob/main/DOWNLOAD.md), and then download the 21 tar.gz archives from Amazon S3 as follows: - -```bash -SHARD_DIR={DESIRED PATH FOR TAR.GZ files} -s5cmd --no-sign-request cp s3://argoai-argoverse/av2/tars/tbv/*.tar.gz ${SHARD_DIR} -``` - -If you would prefer to not install a 3rd party download tool (`s5cmd`), you can use `wget` to download the `tar.gz` files: -```bash -wget https://s3.amazonaws.com/argoai-argoverse/av2/tars/tbv/TbV_v1.0_shard0.tar.gz -wget https://s3.amazonaws.com/argoai-argoverse/av2/tars/tbv/TbV_v1.0_shard1.tar.gz -... -wget https://s3.amazonaws.com/argoai-argoverse/av2/tars/tbv/TbV_v1.0_shard20.tar.gz -``` - -Next, extract TbV tar.gz files that were just downloaded to a local disk using [`untar_tbv.py`](https://github.com/argoai/av2-api/blob/main/tutorials/untar_tbv.py): -```bash -python tutorials/untar_tbv.py -``` -**Not Recommended**: If you want to directly transfer the extracted files, you may use: -```bash -DESIRED_TBV_DATAROOT={DESIRED LOCAL DIRECTORY PATH FOR TBV VEHICLE LOGS} -s5cmd --no-sign-request cp s3://argoai-argoverse/av2/tbv/* ${DESIRED_TBV_DATAROOT} -``` - -## Log Distribution Across Cities -TbV vehicle logs are captured in 6 cities, according to the following distribution: -- Austin, Texas: 80 logs. -- Detroit, Michigan: 139 logs. -- Miami, Florida: 349 logs. -- Pittsburgh, Pennsylvania: 318 logs. -- Palo Alto, California: 21 logs. -- Washington, D.C.: 136 logs. - - -## Baselines -We provide both pre-trained models for HD map change detection and code for training such models at [https://github.com/johnwlambert/tbv](https://github.com/johnwlambert/tbv). - -## Sensor Suite - -The sensor suite is identical to the Argoverse 2 Sensor Dataset, except no stereo sensor data is provided, and the sensor imagery for 6 of the cameras is provided at half of the image resolution (`ring_front_center` is at an identical resolution, however). - -Lidar sweeps are collected at 10 Hz, along with 20 fps imagery from 7 ring cameras positioned to provide a fully panoramic field of view. In addition, camera intrinsics, extrinsics and 6-DOF ego-vehicle pose in a global coordinate system are provided. Lidar returns are captured by two 32-beam lidars, spinning at 10 Hz in the same direction, but separated in orientation by 180°. The cameras trigger in-sync with both lidars, leading to a 20 Hz frame-rate. The seven global shutter cameras are synchronized to the lidar to have their exposure centered on the lidar sweeping through their fields of view. - -We aggregate all returns from the two stacked 32-beam sensors into a single sweep. These sensors each have different, overlapping fields-of-view. Both lidars have their own reference frame, and we refer to them as `up_lidar` and `down_lidar`, respectively. We have egomotion-compensated the LiDAR sensor data to the egovehicle reference nanosecond timestamp. **All LiDAR returns are provided in the egovehicle reference frame, not the individual LiDAR reference frame**. - -TbV imagery is provided at (height x width) of `2048 x 1550` (portrait orientation) for the ring front-center camera, and at `775 x 1024` (landscape orientation) for all other 6 cameras. Please note that the ring front-center camera imagery is provided at higher resolution. **All camera imagery is provided in an undistorted format**. - -

- -

- -## Dataset Structure Format - -Tabular data (lidar sweeps, poses, calibration) are provided as [Apache Feather Files](https://arrow.apache.org/docs/python/feather.html) with the file extension `.feather`. We show examples below. - -Unlike the Argoverse 2 Sensor Dataset, TbV features no object annotations. - -## Maps - -A local vector map and a local ground height raster map is provided per log, please refer to the [Map README](../../map/README.md) for additional details. For example, for log `VvgE5LfOzIahbS266MFW7tP2al00LhQn__Autumn_2020`, the `map` subdirectory contains 3 files: - -- `log_map_archive_VvgE5LfOzIahbS266MFW7tP2al00LhQn__Autumn_2020____DTW_city_73942.json`: local vector map. -- `VvgE5LfOzIahbS266MFW7tP2al00LhQn__Autumn_2020_ground_height_surface____DTW.npy`: local ground height raster map, at 30 cm resolution. -- `VvgE5LfOzIahbS266MFW7tP2al00LhQn__Autumn_2020___img_Sim2_city.json`: mapping from city coordinates to raster grid/array coordinates. - -## Pose - -6-DOF ego-vehicle pose in a global (city) coordinate system is provided (visualized in the figure below as a red line, with red circles indicated at a 1 Hz frequency): -

- -

- -We refer to this pose as `city_SE3_egovehicle` throughout the codebase: - -```python ->>> import av2.utils.io as io_utils ->>> io_utils.read_feather("{TBV_ROOT}/VvgE5LfOzIahbS266MFW7tP2al00LhQn__Autumn_2020/city_SE3_egovehicle.feather") - timestamp_ns qw qx qy qz tx_m ty_m tz_m -0 315969466027482498 0.245655 0.009583 -0.014121 -0.969207 9277.579933 6805.407468 -22.647127 -1 315969466042441191 0.245661 0.009824 -0.014529 -0.969197 9277.496340 6805.362364 -22.647355 -2 315969466057428264 0.245682 0.009999 -0.015003 -0.969183 9277.418457 6805.317208 -22.648150 -3 315969466060265000 0.245687 0.010025 -0.015133 -0.969179 9277.402699 6805.308645 -22.648235 -4 315969466077482496 0.245723 0.010218 -0.015682 -0.969159 9277.306645 6805.257303 -22.648716 -... ... ... ... ... ... ... ... ... -8811 315969525887425441 0.843540 0.008404 -0.005364 -0.536974 9371.218847 6465.181151 -23.095571 -8812 315969525892441193 0.843547 0.008349 -0.005421 -0.536963 9371.243129 6465.129394 -23.097279 -8813 315969525899927216 0.843569 0.008234 -0.005435 -0.536930 9371.278003 6465.054774 -23.097989 -8814 315969525907428274 0.843575 0.008092 -0.005358 -0.536924 9371.312815 6464.980204 -23.098440 -8815 315969525912451243 0.843601 0.008013 -0.005400 -0.536883 9371.333643 6464.934933 -23.095809 - -[8816 rows x 8 columns] -``` - -## LiDAR Sweeps - -For example, we show below the format of an example sweep `sensors/lidar/315969468259945000.feather` (the sweep has a reference timestamp of 315969468259945000 nanoseconds). Unlike the sensor dataset, TbV sweeps **do not** contain timestamps per return (there is no `offset_ns` attribute): - -```python ->>> io_utils.read_feather("{TBV_ROOT}/VvgE5LfOzIahbS266MFW7tP2al00LhQn__Autumn_2020/sensors/lidar/315969468259945000.feather") - x y z intensity laser_number -0 -13.023438 12.492188 -0.138794 103 25 -1 -10.992188 10.726562 1.831055 36 7 -2 -15.273438 14.460938 0.356445 35 23 -3 -10.828125 10.609375 1.076172 49 19 -4 -10.570312 10.421875 1.456055 104 3 -... ... ... ... ... ... -89261 4.136719 -2.882812 1.631836 0 19 -89262 4.054688 -2.783203 1.546875 23 3 -89263 60.312500 -77.937500 10.671875 47 25 -89264 17.984375 -21.390625 1.214844 6 7 -89265 4.160156 -2.953125 1.719727 36 23 - -[89266 rows x 5 columns] -``` - -## Calibration - -An example calibration file is shown below, parameterizing `vehicle_SE3_sensor` for each sensor (the sensor's pose in the egovehicle coordinate system): - -```python ->>> io_utils.read_feather("{TBV_ROOT}/VvgE5LfOzIahbS266MFW7tP2al00LhQn__Autumn_2020/calibration/egovehicle_SE3_sensor.feather") - sensor_name qw qx qy qz tx_m ty_m tz_m -0 ring_front_center 0.501067 -0.499697 0.501032 -0.498200 1.626286 -0.020252 1.395709 -1 ring_front_left 0.635731 -0.671186 0.277021 -0.261946 1.549577 0.177582 1.388212 -2 ring_front_right 0.262148 -0.277680 0.670922 -0.635638 1.546437 -0.216452 1.394248 -3 ring_rear_left 0.602832 -0.602666 -0.368113 0.371322 1.099130 0.106534 1.389519 -4 ring_rear_right 0.371203 -0.367863 -0.601619 0.604103 1.101165 -0.141049 1.399768 -5 ring_side_left 0.686808 -0.722414 -0.058060 0.055145 1.308706 0.255756 1.379285 -6 ring_side_right 0.055626 -0.056105 -0.722917 0.686403 1.306407 -0.291250 1.394200 -7 up_lidar 0.999995 0.000000 0.000000 -0.003215 1.350110 -0.013707 1.640420 -8 down_lidar 0.000080 -0.994577 0.103998 0.000039 1.355172 -0.021696 1.507259 -``` - -## Intrinsics - -An example camera intrinsics file is shown below: - -```python ->>> io_utils.read_feather("{TBV_ROOT}/VvgE5LfOzIahbS266MFW7tP2al00LhQn__Autumn_2020/calibration/intrinsics.feather") - sensor_name fx_px fy_px cx_px cy_px k1 k2 k3 height_px width_px -0 ring_front_center 1686.020228 1686.020228 775.467979 1020.785939 -0.245028 -0.196287 0.301861 2048 1550 -1 ring_front_left 842.323546 842.323546 513.397368 387.828521 -0.262302 -0.108561 0.179488 775 1024 -2 ring_front_right 842.813516 842.813516 514.154170 387.181497 -0.257722 -0.125524 0.199077 775 1024 -3 ring_rear_left 841.669682 841.669682 513.211190 387.324359 -0.257018 -0.130649 0.204405 775 1024 -4 ring_rear_right 843.832813 843.832813 512.201788 387.673600 -0.256830 -0.132244 0.208272 775 1024 -5 ring_side_left 842.178507 842.178507 512.314602 388.188297 -0.256152 -0.131642 0.205564 775 1024 -6 ring_side_right 842.703781 842.703781 513.191605 386.876520 -0.260558 -0.110271 0.179140 775 1024 -``` - -## Privacy - -All faces and license plates, whether inside vehicles or outside of the drivable area, are blurred extensively to preserve privacy. diff --git a/src/av2/evaluation/__init__.py b/src/av2/evaluation/__init__.py index 8109497b..c4fb98fb 100644 --- a/src/av2/evaluation/__init__.py +++ b/src/av2/evaluation/__init__.py @@ -1,3 +1,40 @@ # """Dataset evaluation subpackage.""" + +from enum import Enum, unique +from typing import Final + +NUM_RECALL_SAMPLES: Final = 101 + + +@unique +class SensorCompetitionCategories(str, Enum): + """Sensor dataset annotation categories.""" + + ARTICULATED_BUS = "ARTICULATED_BUS" + BICYCLE = "BICYCLE" + BICYCLIST = "BICYCLIST" + BOLLARD = "BOLLARD" + BOX_TRUCK = "BOX_TRUCK" + BUS = "BUS" + CONSTRUCTION_BARREL = "CONSTRUCTION_BARREL" + CONSTRUCTION_CONE = "CONSTRUCTION_CONE" + DOG = "DOG" + LARGE_VEHICLE = "LARGE_VEHICLE" + MESSAGE_BOARD_TRAILER = "MESSAGE_BOARD_TRAILER" + MOBILE_PEDESTRIAN_CROSSING_SIGN = "MOBILE_PEDESTRIAN_CROSSING_SIGN" + MOTORCYCLE = "MOTORCYCLE" + MOTORCYCLIST = "MOTORCYCLIST" + PEDESTRIAN = "PEDESTRIAN" + REGULAR_VEHICLE = "REGULAR_VEHICLE" + SCHOOL_BUS = "SCHOOL_BUS" + SIGN = "SIGN" + STOP_SIGN = "STOP_SIGN" + STROLLER = "STROLLER" + TRUCK = "TRUCK" + TRUCK_CAB = "TRUCK_CAB" + VEHICULAR_TRAILER = "VEHICULAR_TRAILER" + WHEELCHAIR = "WHEELCHAIR" + WHEELED_DEVICE = "WHEELED_DEVICE" + WHEELED_RIDER = "WHEELED_RIDER" diff --git a/src/av2/evaluation/detection/SUBMISSION_FORMAT.md b/src/av2/evaluation/detection/SUBMISSION_FORMAT.md deleted file mode 100644 index 38312295..00000000 --- a/src/av2/evaluation/detection/SUBMISSION_FORMAT.md +++ /dev/null @@ -1,46 +0,0 @@ -# 3D Object Detection Submission Format - -The evaluation expects the following fields within a `pandas.DataFrame`: - -- `tx_m`: x-component of the object translation in the egovehicle reference frame. -- `ty_m`: y-component of the object translation in the egovehicle reference frame. -- `tz_m`: z-component of the object translation in the egovehicle reference frame. -- `length_m`: Object extent along the x-axis in meters. -- `width_m`: Object extent along the y-axis in meters. -- `height_m`: Object extent along the z-axis in meters. -- `qw`: Real quaternion coefficient. -- `qx`: First quaternion coefficient. -- `qy`: Second quaternion coefficient. -- `qz`: Third quaternion coefficient. -- `score`: Object confidence. -- `log_id`: Log id associated with the detection. -- `timestamp_ns`: Timestamp associated with the detection. -- `category`: Object category. - -An example looks like this: - -```python -# These detections are only for example purposes. - -display(detections) # Detections is type `pd.DataFrame` - tx_m ty_m tz_m length_m width_m height_m qw qx qy qz score log_id timestamp_ns category -0 -162.932968 1.720428 0.039064 1.596262 0.772320 1.153996 0.125843 0.0 0.0 0.992050 0.127634 b0116f1c-f88f-3c09-b4bf-fc3c8ebeda56 315968193659921000 WHEELCHAIR -1 -120.362213 19.875946 -0.382618 1.441901 0.593825 1.199819 0.802836 0.0 0.0 0.596200 0.126565 b0116f1c-f88f-3c09-b4bf-fc3c8ebeda56 315968193659921000 BICYCLE -... -14000000 10.182907 29.489899 0.662969 9.166531 1.761454 1.615999 0.023469 0.0 0.0 -0.999725 0.322177 b2d9d8a5-847b-3c3b-aed1-c414319d20af 315978610360111000 REGULAR_VEHICLE - -detections.columns -Index(['tx_m', 'ty_m', 'tz_m', 'length_m', 'width_m', 'height_m', 'qw', 'qx', - 'qy', 'qz', 'score', 'log_id', 'timestamp_ns', 'category'], - dtype='object') -``` - -We need to export the above dataframe for submission. This can be done by: - -```python -import pandas as pd - -detections.to_feather("detections.feather") -``` - -Lastly, submit this file to the competition leaderboard. \ No newline at end of file diff --git a/src/av2/evaluation/detection/constants.py b/src/av2/evaluation/detection/constants.py index f0181adb..9b5157bf 100644 --- a/src/av2/evaluation/detection/constants.py +++ b/src/av2/evaluation/detection/constants.py @@ -3,30 +3,226 @@ """3D object detection evaluation constants.""" from enum import Enum, unique -from typing import Final, List +from typing import Dict, Final, Tuple from av2.utils.constants import PI -MAX_SCALE_ERROR: Final[float] = 1.0 -MAX_YAW_RAD_ERROR: Final[float] = PI +MAX_SCALE_ERROR: Final = 1.0 +MAX_YAW_RAD_ERROR: Final = PI # Higher is better. -MIN_AP: Final[float] = 0.0 -MIN_CDS: Final[float] = 0.0 +MIN_AP: Final = 0.0 +MIN_CDS: Final = 0.0 # Lower is better. -MAX_NORMALIZED_ATE: Final[float] = 1.0 -MAX_NORMALIZED_ASE: Final[float] = 1.0 -MAX_NORMALIZED_AOE: Final[float] = 1.0 +MAX_NORMALIZED_ATE: Final = 1.0 +MAX_NORMALIZED_ASE: Final = 1.0 +MAX_NORMALIZED_AOE: Final = 1.0 # Max number of boxes considered per class per scene. -MAX_NUM_BOXES: Final[int] = 500 +MAX_NUM_BOXES: Final = 500 -NUM_DECIMALS: Final[int] = 3 +NUM_DECIMALS: Final = 3 -TRANSLATION_COLS: Final[List[str]] = ["tx_m", "ty_m", "tz_m"] -DIMENSION_COLS: Final[List[str]] = ["length_m", "width_m", "height_m"] -QUAT_WXYZ_COLS: Final[List[str]] = ["qw", "qx", "qy", "qz"] +TRANSLATION_COLS: Final = ( + "tx_m", + "ty_m", + "tz_m", +) +DIMENSION_COLS: Final = ( + "length_m", + "width_m", + "height_m", +) +QUAT_WXYZ_COLS: Final = ( + "qw", + "qx", + "qy", + "qz", +) + +HIERARCHY: Final[Dict[str, Tuple[str, ...]]] = { + "FINEGRAIN": ( + "REGULAR_VEHICLE", + "LARGE_VEHICLE", + "BUS", + "BOX_TRUCK", + "TRUCK", + "VEHICULAR_TRAILER", + "TRUCK_CAB", + "SCHOOL_BUS", + "ARTICULATED_BUS", + "PEDESTRIAN", + "WHEELED_RIDER", + "BICYCLE", + "BICYCLIST", + "MOTORCYCLE", + "MOTORCYCLIST", + "WHEELED_DEVICE", + "WHEELED_RIDER", + "WHEELCHAIR", + "STROLLER", + "DOG", + "BOLLARD", + "CONSTRUCTION_CONE", + "SIGN", + "CONSTRUCTION_BARREL", + "STOP_SIGN", + "MOBILE_PEDESTRIAN_CROSSING_SIGN", + "MESSAGE_BOARD_TRAILER", + ), + "GROUP": ( + "VEHICLE", + "VEHICLE", + "VEHICLE", + "VEHICLE", + "VEHICLE", + "VEHICLE", + "VEHICLE", + "VEHICLE", + "VEHICLE", + "VULNERABLE", + "VULNERABLE", + "VULNERABLE", + "VULNERABLE", + "VULNERABLE", + "VULNERABLE", + "VULNERABLE", + "VULNERABLE", + "VULNERABLE", + "VULNERABLE", + "VULNERABLE", + "MOVABLE", + "MOVABLE", + "MOVABLE", + "MOVABLE", + "MOVABLE", + "MOVABLE", + "MOVABLE", + ), + "OBJECT": ( + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + "OBJECT", + ), +} + +LCA: Final[Dict[str, Tuple[str, ...]]] = { + "ARTICULATED_BUS": ("ARTICULATED_BUS",), + "BICYCLE": ("BICYCLE",), + "BICYCLIST": ("BICYCLIST",), + "BOLLARD": ("BOLLARD",), + "BOX_TRUCK": ("BOX_TRUCK",), + "BUS": ("BUS",), + "CONSTRUCTION_BARREL": ("CONSTRUCTION_BARREL",), + "CONSTRUCTION_CONE": ("CONSTRUCTION_CONE",), + "DOG": ("DOG",), + "LARGE_VEHICLE": ("LARGE_VEHICLE",), + "MESSAGE_BOARD_TRAILER": ("MESSAGE_BOARD_TRAILER",), + "MOBILE_PEDESTRIAN_CROSSING_SIGN": ("MOBILE_PEDESTRIAN_CROSSING_SIGN",), + "MOTORCYCLE": ("MOTORCYCLE",), + "MOTORCYCLIST": ("MOTORCYCLIST",), + "PEDESTRIAN": ("PEDESTRIAN",), + "REGULAR_VEHICLE": ("REGULAR_VEHICLE",), + "SCHOOL_BUS": ("SCHOOL_BUS",), + "SIGN": ("SIGN",), + "STOP_SIGN": ("STOP_SIGN",), + "STROLLER": ("STROLLER",), + "TRUCK": ("TRUCK",), + "TRUCK_CAB": ("TRUCK_CAB",), + "VEHICULAR_TRAILER": ("VEHICULAR_TRAILER",), + "WHEELCHAIR": ("WHEELCHAIR",), + "WHEELED_DEVICE": ("WHEELED_DEVICE",), + "WHEELED_RIDER": ("WHEELED_RIDER",), + "VEHICLE": ( + "REGULAR_VEHICLE", + "LARGE_VEHICLE", + "BUS", + "BOX_TRUCK", + "TRUCK", + "VEHICULAR_TRAILER", + "TRUCK_CAB", + "SCHOOL_BUS", + "ARTICULATED_BUS", + ), + "VULNERABLE": ( + "PEDESTRIAN", + "WHEELED_RIDER", + "BICYCLE", + "BICYCLIST", + "MOTORCYCLE", + "MOTORCYCLIST", + "WHEELED_DEVICE", + "WHEELED_RIDER", + "WHEELCHAIR", + "STROLLER", + "DOG", + ), + "MOVABLE": ( + "BOLLARD", + "CONSTRUCTION_CONE", + "SIGN", + "CONSTRUCTION_BARREL", + "STOP_SIGN", + "MOBILE_PEDESTRIAN_CROSSING_SIGN", + "MESSAGE_BOARD_TRAILER", + ), + "OBJECT": ( + "REGULAR_VEHICLE", + "LARGE_VEHICLE", + "BUS", + "BOX_TRUCK", + "TRUCK", + "VEHICULAR_TRAILER", + "TRUCK_CAB", + "SCHOOL_BUS", + "ARTICULATED_BUS", + "PEDESTRIAN", + "WHEELED_RIDER", + "BICYCLE", + "BICYCLIST", + "MOTORCYCLE", + "MOTORCYCLIST", + "WHEELED_DEVICE", + "WHEELED_RIDER", + "WHEELCHAIR", + "STROLLER", + "DOG", + "BOLLARD", + "CONSTRUCTION_CONE", + "SIGN", + "CONSTRUCTION_BARREL", + "STOP_SIGN", + "MOBILE_PEDESTRIAN_CROSSING_SIGN", + "MESSAGE_BOARD_TRAILER", + ), +} + +LCA_COLUMNS: Final = ("LCA=0", "LCA=1", "LCA=2") @unique @@ -49,38 +245,6 @@ class MetricNames(str, Enum): CDS = "CDS" -@unique -class CompetitionCategories(str, Enum): - """Sensor dataset annotation categories.""" - - ARTICULATED_BUS = "ARTICULATED_BUS" - BICYCLE = "BICYCLE" - BICYCLIST = "BICYCLIST" - BOLLARD = "BOLLARD" - BOX_TRUCK = "BOX_TRUCK" - BUS = "BUS" - CONSTRUCTION_BARREL = "CONSTRUCTION_BARREL" - CONSTRUCTION_CONE = "CONSTRUCTION_CONE" - DOG = "DOG" - LARGE_VEHICLE = "LARGE_VEHICLE" - MESSAGE_BOARD_TRAILER = "MESSAGE_BOARD_TRAILER" - MOBILE_PEDESTRIAN_CROSSING_SIGN = "MOBILE_PEDESTRIAN_CROSSING_SIGN" - MOTORCYCLE = "MOTORCYCLE" - MOTORCYCLIST = "MOTORCYCLIST" - PEDESTRIAN = "PEDESTRIAN" - REGULAR_VEHICLE = "REGULAR_VEHICLE" - SCHOOL_BUS = "SCHOOL_BUS" - SIGN = "SIGN" - STOP_SIGN = "STOP_SIGN" - STROLLER = "STROLLER" - TRUCK = "TRUCK" - TRUCK_CAB = "TRUCK_CAB" - VEHICULAR_TRAILER = "VEHICULAR_TRAILER" - WHEELCHAIR = "WHEELCHAIR" - WHEELED_DEVICE = "WHEELED_DEVICE" - WHEELED_RIDER = "WHEELED_RIDER" - - @unique class AffinityType(str, Enum): """Affinity types for assigning detections to ground truth cuboids.""" diff --git a/src/av2/evaluation/detection/eval.py b/src/av2/evaluation/detection/eval.py index 7d95ff36..18cd8438 100644 --- a/src/av2/evaluation/detection/eval.py +++ b/src/av2/evaluation/detection/eval.py @@ -52,34 +52,46 @@ e.g. AP, ATE, ASE, AOE, CDS by default. """ import logging -from multiprocessing import get_context -from typing import Dict, Final, List, Optional, Tuple +import multiprocessing as mp +import warnings +from typing import Any, Dict, Final, List, Optional, Tuple, cast import numpy as np import pandas as pd - -from av2.evaluation.detection.constants import NUM_DECIMALS, MetricNames, TruePositiveErrorNames +import polars as pl +from tqdm import tqdm + +from av2.evaluation.detection.constants import ( + HIERARCHY, + LCA, + LCA_COLUMNS, + NUM_DECIMALS, + MetricNames, + TruePositiveErrorNames, +) from av2.evaluation.detection.utils import ( DetectionCfg, accumulate, + accumulate_hierarchy, compute_average_precision, - groupby, + is_evaluated, load_mapped_avm_and_egoposes, ) from av2.geometry.se3 import SE3 from av2.map.map_api import ArgoverseStaticMap from av2.structures.cuboid import ORDERED_CUBOID_COL_NAMES from av2.utils.io import TimestampedCitySE3EgoPoses -from av2.utils.typing import NDArrayBool, NDArrayFloat - -TP_ERROR_COLUMNS: Final[Tuple[str, ...]] = tuple(x.value for x in TruePositiveErrorNames) -DTS_COLUMN_NAMES: Final[Tuple[str, ...]] = tuple(ORDERED_CUBOID_COL_NAMES) + ("score",) -GTS_COLUMN_NAMES: Final[Tuple[str, ...]] = tuple(ORDERED_CUBOID_COL_NAMES) + ("num_interior_pts",) -UUID_COLUMN_NAMES: Final[Tuple[str, ...]] = ( - "log_id", - "timestamp_ns", - "category", -) +from av2.utils.typing import NDArrayBool, NDArrayFloat, NDArrayObject + +warnings.filterwarnings("ignore", module="google") + +TP_ERROR_COLUMNS: Final = tuple(x.value for x in TruePositiveErrorNames) +DTS_COLUMNS: Final = tuple(ORDERED_CUBOID_COL_NAMES) + ("score",) +GTS_COLUMNS: Final = tuple(ORDERED_CUBOID_COL_NAMES) + ("num_interior_pts",) + +UUID_COLUMNS: Final = ("log_id", "timestamp_ns") +CATEGORY_COLUMN: Final = ("category",) +DETECTION_UUID_COLUMNS: Final = UUID_COLUMNS + CATEGORY_COLUMN logger = logging.getLogger(__name__) @@ -105,7 +117,6 @@ def evaluate( K refers to the number of evaluation metrics. Raises: - RuntimeError: If accumulation fails. ValueError: If ROI pruning is enabled but a dataset directory is not specified. """ if cfg.eval_only_roi_instances and cfg.dataset_dir is None: @@ -115,21 +126,24 @@ def evaluate( ) # Sort both the detections and annotations by lexicographic order for grouping. - dts = dts.sort_values(list(UUID_COLUMN_NAMES)) - gts = gts.sort_values(list(UUID_COLUMN_NAMES)) - - dts_npy: NDArrayFloat = dts[list(DTS_COLUMN_NAMES)].to_numpy() - gts_npy: NDArrayFloat = gts[list(GTS_COLUMN_NAMES)].to_numpy() - - dts_uuids: List[str] = dts[list(UUID_COLUMN_NAMES)].to_numpy().tolist() - gts_uuids: List[str] = gts[list(UUID_COLUMN_NAMES)].to_numpy().tolist() - - # We merge the unique identifier -- the tuple of ("log_id", "timestamp_ns", "category") - # into a single string to optimize the subsequent grouping operation. - # `groupby_mapping` produces a mapping from the uuid to the group of detections / annotations - # which fall into that group. - uuid_to_dts = groupby([":".join(map(str, x)) for x in dts_uuids], dts_npy) - uuid_to_gts = groupby([":".join(map(str, x)) for x in gts_uuids], gts_npy) + dts = dts.sort_values(list(DETECTION_UUID_COLUMNS)) + gts = gts.sort_values(list(DETECTION_UUID_COLUMNS)) + + dts_pl = pl.from_pandas(dts) + gts_pl = pl.from_pandas(gts) + + uuid_to_dts = { + k: v[list(DTS_COLUMNS)].to_numpy().astype(float) + for k, v in dts_pl.partition_by( + DETECTION_UUID_COLUMNS, maintain_order=True, as_dict=True + ).items() + } + uuid_to_gts = { + k: v[list(GTS_COLUMNS)].to_numpy().astype(float) + for k, v in gts_pl.partition_by( + DETECTION_UUID_COLUMNS, maintain_order=True, as_dict=True + ).items() + } log_id_to_avm: Optional[Dict[str, ArgoverseStaticMap]] = None log_id_to_timestamped_poses: Optional[Dict[str, TimestampedCitySE3EgoPoses]] = None @@ -138,13 +152,29 @@ def evaluate( if cfg.eval_only_roi_instances and cfg.dataset_dir is not None: logger.info("Loading maps and egoposes ...") log_ids: List[str] = gts.loc[:, "log_id"].unique().tolist() - log_id_to_avm, log_id_to_timestamped_poses = load_mapped_avm_and_egoposes(log_ids, cfg.dataset_dir) + log_id_to_avm, log_id_to_timestamped_poses = load_mapped_avm_and_egoposes( + log_ids, cfg.dataset_dir + ) - args_list: List[Tuple[NDArrayFloat, NDArrayFloat, DetectionCfg, Optional[ArgoverseStaticMap], Optional[SE3]]] = [] + accumulate_args_list: List[ + Tuple[ + NDArrayFloat, + NDArrayFloat, + DetectionCfg, + Optional[ArgoverseStaticMap], + Optional[SE3], + ] + ] = [] uuids = sorted(uuid_to_dts.keys() | uuid_to_gts.keys()) for uuid in uuids: - log_id, timestamp_ns, _ = uuid.split(":") - args: Tuple[NDArrayFloat, NDArrayFloat, DetectionCfg, Optional[ArgoverseStaticMap], Optional[SE3]] + log_id, timestamp_ns, _ = uuid + args: Tuple[ + NDArrayFloat, + NDArrayFloat, + DetectionCfg, + Optional[ArgoverseStaticMap], + Optional[SE3], + ] sweep_dts: NDArrayFloat = np.zeros((0, 10)) sweep_gts: NDArrayFloat = np.zeros((0, 10)) @@ -158,19 +188,21 @@ def evaluate( avm = log_id_to_avm[log_id] city_SE3_ego = log_id_to_timestamped_poses[log_id][int(timestamp_ns)] args = sweep_dts, sweep_gts, cfg, avm, city_SE3_ego - args_list.append(args) + accumulate_args_list.append(args) logger.info("Starting evaluation ...") - with get_context("spawn").Pool(processes=n_jobs) as p: - outputs: Optional[List[Tuple[NDArrayFloat, NDArrayFloat]]] = p.starmap(accumulate, args_list) + with mp.get_context("spawn").Pool(processes=n_jobs) as p: + outputs: List[Tuple[NDArrayFloat, NDArrayFloat]] = p.starmap( + accumulate, accumulate_args_list + ) - if outputs is None: - raise RuntimeError("Accumulation has failed! Please check the integrity of your detections and annotations.") dts_list, gts_list = zip(*outputs) - METRIC_COLUMN_NAMES = cfg.affinity_thresholds_m + TP_ERROR_COLUMNS + ("is_evaluated",) - dts_metrics: NDArrayFloat = np.concatenate(dts_list) # type: ignore - gts_metrics: NDArrayFloat = np.concatenate(gts_list) # type: ignore + METRIC_COLUMN_NAMES = ( + cfg.affinity_thresholds_m + TP_ERROR_COLUMNS + ("is_evaluated",) + ) + dts_metrics: NDArrayFloat = np.concatenate(dts_list) + gts_metrics: NDArrayFloat = np.concatenate(gts_list) dts.loc[:, METRIC_COLUMN_NAMES] = dts_metrics gts.loc[:, METRIC_COLUMN_NAMES] = gts_metrics @@ -197,14 +229,19 @@ def summarize_metrics( The summary metrics. """ # Sample recall values in the [0, 1] interval. - recall_interpolated: NDArrayFloat = np.linspace(0, 1, cfg.num_recall_samples, endpoint=True) + recall_interpolated: NDArrayFloat = np.linspace( + 0, 1, cfg.num_recall_samples, endpoint=True + ) # Initialize the summary metrics. summary = pd.DataFrame( - {s.value: cfg.metrics_defaults[i] for i, s in enumerate(tuple(MetricNames))}, index=cfg.categories + {s.value: cfg.metrics_defaults[i] for i, s in enumerate(tuple(MetricNames))}, + index=cfg.categories, ) - average_precisions = pd.DataFrame({t: 0.0 for t in cfg.affinity_thresholds_m}, index=cfg.categories) + average_precisions = pd.DataFrame( + {t: 0.0 for t in cfg.affinity_thresholds_m}, index=cfg.categories + ) for category in cfg.categories: # Find detections that have the current category. is_category_dts = dts["category"] == category @@ -213,7 +250,11 @@ def summarize_metrics( is_valid_dts = np.logical_and(is_category_dts, dts["is_evaluated"]) # Get valid detections and sort them in descending order. - category_dts = dts.loc[is_valid_dts].sort_values(by="score", ascending=False).reset_index(drop=True) + category_dts = ( + dts.loc[is_valid_dts] + .sort_values(by="score", ascending=False) + .reset_index(drop=True) + ) # Find annotations that have the current category. is_category_gts = gts["category"] == category @@ -226,19 +267,27 @@ def summarize_metrics( continue for affinity_threshold_m in cfg.affinity_thresholds_m: - true_positives: NDArrayBool = category_dts[affinity_threshold_m].astype(bool).to_numpy() + true_positives: NDArrayBool = ( + category_dts[affinity_threshold_m].astype(bool).to_numpy() + ) # Continue if there aren't any true positives. if len(true_positives) == 0: continue # Compute average precision for the current threshold. - threshold_average_precision, _ = compute_average_precision(true_positives, recall_interpolated, num_gts) + threshold_average_precision, _ = compute_average_precision( + true_positives, recall_interpolated, num_gts + ) # Record the average precision. - average_precisions.loc[category, affinity_threshold_m] = threshold_average_precision + average_precisions.loc[category, affinity_threshold_m] = ( + threshold_average_precision + ) - mean_average_precisions: NDArrayFloat = average_precisions.loc[category].to_numpy().mean() + mean_average_precisions: NDArrayFloat = ( + average_precisions.loc[category].to_numpy().mean() + ) # Select only the true positives for each instance. middle_idx = len(cfg.affinity_thresholds_m) // 2 @@ -265,3 +314,233 @@ def summarize_metrics( # Return the summary. return summary + + +def evaluate_hierarchy( + dts: pd.DataFrame, + gts: pd.DataFrame, + cfg: DetectionCfg, + n_jobs: int = 8, +) -> pd.DataFrame: + """Evaluate a set of detections against the ground truth annotations. + + Each sweep is processed independently, computing assignment between detections and ground truth annotations. + + Args: + dts: (N,14) Table of detections. + gts: (M,15) Table of ground truth annotations. + cfg: Detection configuration. + n_jobs: Number of jobs running concurrently during evaluation. + + Returns: + (C+1,3) Pandas DataFrame for the Hierarchical AP at LCA = 0, 1, 2 for all classes. The last row reports the average over all classes. + + Raises: + RuntimeError: If accumulation fails. + ValueError: If ROI pruning is enabled but a dataset directory is not specified. + """ + if cfg.eval_only_roi_instances and cfg.dataset_dir is None: + raise ValueError( + "ROI pruning has been enabled, but the dataset directory has not be specified. " + "Please set `dataset_directory` to the split root, e.g. av2/sensor/val." + ) + + # Sort both the detections and annotations by lexicographic order for grouping. + dts = dts.sort_values(list(UUID_COLUMNS)) + gts = gts.sort_values(list(UUID_COLUMNS)) + + dts_categories = dts[list(CATEGORY_COLUMN)].to_numpy().astype(str) + gts_categories = gts[list(CATEGORY_COLUMN)].to_numpy().astype(str) + + dts_pl = pl.from_pandas(dts) + gts_pl = pl.from_pandas(gts) + + uuid_to_dts = { + cast(Tuple[str, int], k): v[list(DTS_COLUMNS)].to_numpy().astype(np.float64) + for k, v in dts_pl.partition_by( + UUID_COLUMNS, maintain_order=True, as_dict=True + ).items() + } + uuid_to_gts = { + cast(Tuple[str, int], k): v[list(GTS_COLUMNS)].to_numpy().astype(np.float64) + for k, v in gts_pl.partition_by( + UUID_COLUMNS, maintain_order=True, as_dict=True + ).items() + } + + uuid_to_dts_cats = { + cast(Tuple[str, int], k): v[list(CATEGORY_COLUMN)].to_numpy().astype(np.object_) + for k, v in dts_pl.partition_by( + UUID_COLUMNS, maintain_order=True, as_dict=True + ).items() + } + + uuid_to_gts_cats = { + cast(Tuple[str, int], k): v[list(CATEGORY_COLUMN)].to_numpy().astype(np.object_) + for k, v in gts_pl.partition_by( + UUID_COLUMNS, maintain_order=True, as_dict=True + ).items() + } + + log_id_to_avm: Optional[Dict[str, ArgoverseStaticMap]] = None + log_id_to_timestamped_poses: Optional[Dict[str, TimestampedCitySE3EgoPoses]] = None + + # Load maps and egoposes if roi-pruning is enabled. + if cfg.eval_only_roi_instances and cfg.dataset_dir is not None: + logger.info("Loading maps and egoposes ...") + log_ids: List[str] = gts.loc[:, "log_id"].unique().tolist() + log_id_to_avm, log_id_to_timestamped_poses = load_mapped_avm_and_egoposes( + log_ids, cfg.dataset_dir + ) + + is_evaluated_args_list: List[ + Tuple[ + NDArrayFloat, + NDArrayFloat, + NDArrayObject, + NDArrayObject, + Tuple[str, int], + DetectionCfg, + Optional[ArgoverseStaticMap], + Optional[SE3], + ] + ] = [] + uuids: List[Tuple[str, int]] = sorted(uuid_to_dts.keys() | uuid_to_gts.keys()) + for uuid in uuids: + log_id, timestamp_ns = uuid + sweep_dts: NDArrayFloat = np.zeros((0, 10)) + sweep_gts: NDArrayFloat = np.zeros((0, 10)) + + sweep_dts_categories = np.zeros((0, 1), dtype=np.object_) + sweep_gts_categories = np.zeros((0, 1), dtype=np.object_) + + if uuid in uuid_to_dts: + sweep_dts = uuid_to_dts[uuid] + sweep_dts_categories = uuid_to_dts_cats[uuid] + + if uuid in uuid_to_gts: + sweep_gts = uuid_to_gts[uuid] + sweep_gts_categories = uuid_to_gts_cats[uuid] + + args: Tuple[ + NDArrayFloat, + NDArrayFloat, + NDArrayObject, + NDArrayObject, + Tuple[str, int], + DetectionCfg, + Optional[ArgoverseStaticMap], + Optional[SE3], + ] = ( + sweep_dts, + sweep_gts, + sweep_dts_categories, + sweep_gts_categories, + uuid, + cfg, + None, + None, + ) + if log_id_to_avm is not None and log_id_to_timestamped_poses is not None: + avm = log_id_to_avm[log_id] + city_SE3_ego = log_id_to_timestamped_poses[log_id][int(timestamp_ns)] + args = ( + sweep_dts, + sweep_gts, + sweep_dts_categories, + sweep_gts_categories, + uuid, + cfg, + avm, + city_SE3_ego, + ) + is_evaluated_args_list.append(args) + + logger.info("Starting evaluation ...") + with mp.get_context("spawn").Pool(processes=n_jobs) as p: + outputs: List[ + Tuple[ + NDArrayFloat, + NDArrayFloat, + NDArrayObject, + NDArrayObject, + Tuple[str, int], + ] + ] = p.starmap(is_evaluated, is_evaluated_args_list) + + dts_list: List[NDArrayFloat] = [] + gts_list: List[NDArrayFloat] = [] + dts_uuids_list: List[Tuple[str, int]] = [] + gts_uuids_list: List[Tuple[str, int]] = [] + dts_categories_list, gts_categories_list = [], [] + for ( + sweep_dts, + sweep_gts, + sweep_dts_categories, + sweep_gts_categories, + uuid, + ) in outputs: + dts_list.append(sweep_dts) + gts_list.append(sweep_gts) + dts_categories_list.append(sweep_dts_categories) + gts_categories_list.append(sweep_gts_categories) + + num_dts = len(sweep_dts) + num_gts = len(sweep_gts) + dts_uuids_list.extend(num_dts * [uuid]) + gts_uuids_list.extend(num_gts * [uuid]) + + dts_npy = np.concatenate(dts_list).astype(np.float64) + gts_npy = np.concatenate(gts_list).astype(np.float64) + dts_categories_npy = np.concatenate(dts_categories_list).astype(np.object_) + gts_categories_npy = np.concatenate(gts_categories_list).astype(np.object_) + dts_uuids_npy = np.array(dts_uuids_list) + gts_uuids_npy = np.array(gts_uuids_list) + + accumulate_hierarchy_args_list: List[ + Tuple[ + NDArrayFloat, + NDArrayFloat, + NDArrayObject, + NDArrayObject, + NDArrayObject, + NDArrayObject, + str, + Tuple[str, ...], + str, + DetectionCfg, + ] + ] = [] + for category in cfg.categories: + index = HIERARCHY["FINEGRAIN"].index(category) + for super_category, categories in HIERARCHY.items(): + lca_category = LCA[categories[index]] + accumulate_hierarchy_args_list.append( + ( + dts_npy, + gts_npy, + dts_categories_npy, + gts_categories_npy, + dts_uuids_npy, + gts_uuids_npy, + category, + lca_category, + super_category, + cfg, + ) + ) + + logger.info("Starting evaluation ...") + accumulate_outputs = [] + for accumulate_args in tqdm(accumulate_hierarchy_args_list): + accumulate_outputs.append(accumulate_hierarchy(*accumulate_args)) + + super_categories = list(HIERARCHY.keys()) + metrics = np.zeros((len(cfg.categories), len(HIERARCHY.keys()))) + for ap, category, super_category in accumulate_outputs: + category_index = cfg.categories.index(category) + super_category_index = super_categories.index(super_category) + metrics[category_index][super_category_index] = round(ap, NUM_DECIMALS) + + metrics = pd.DataFrame(metrics, columns=LCA_COLUMNS, index=cfg.categories) + return metrics diff --git a/src/av2/evaluation/detection/utils.py b/src/av2/evaluation/detection/utils.py index 412e8428..60266f2e 100644 --- a/src/av2/evaluation/detection/utils.py +++ b/src/av2/evaluation/detection/utils.py @@ -13,20 +13,22 @@ import logging from dataclasses import dataclass from pathlib import Path -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Set, Tuple, Union import numpy as np from joblib import Parallel, delayed from scipy.spatial.distance import cdist +from upath import UPath +from av2.evaluation import NUM_RECALL_SAMPLES, SensorCompetitionCategories from av2.evaluation.detection.constants import ( MAX_NORMALIZED_ASE, MAX_SCALE_ERROR, MAX_YAW_RAD_ERROR, MIN_AP, MIN_CDS, + NUM_DECIMALS, AffinityType, - CompetitionCategories, DistanceType, FilterMetricType, InterpType, @@ -38,7 +40,7 @@ from av2.structures.cuboid import Cuboid, CuboidList from av2.utils.constants import EPS from av2.utils.io import TimestampedCitySE3EgoPoses, read_city_SE3_ego -from av2.utils.typing import NDArrayBool, NDArrayFloat, NDArrayInt +from av2.utils.typing import NDArrayBool, NDArrayFloat, NDArrayInt, NDArrayObject logger = logging.getLogger(__name__) @@ -62,13 +64,13 @@ class DetectionCfg: affinity_thresholds_m: Tuple[float, ...] = (0.5, 1.0, 2.0, 4.0) affinity_type: AffinityType = AffinityType.CENTER - categories: Tuple[str, ...] = tuple(x.value for x in CompetitionCategories) - dataset_dir: Optional[Path] = None + categories: Tuple[str, ...] = tuple(x.value for x in SensorCompetitionCategories) + dataset_dir: Optional[Union[Path, UPath]] = None eval_only_roi_instances: bool = True filter_metric: FilterMetricType = FilterMetricType.EUCLIDEAN max_num_dts_per_category: int = 100 max_range_m: float = 150.0 - num_recall_samples: int = 100 + num_recall_samples: int = NUM_RECALL_SAMPLES tp_threshold_m: float = 2.0 @property @@ -142,7 +144,9 @@ def accumulate( is_evaluated_gts &= compute_objects_in_roi_mask(gts, city_SE3_ego, avm) is_evaluated_dts &= compute_evaluated_dts_mask(dts[..., :3], cfg) - is_evaluated_gts &= compute_evaluated_gts_mask(gts[..., :3], gts[..., -1], cfg) + is_evaluated_gts &= compute_evaluated_gts_mask( + gts[..., :3], gts[..., -1].astype(int), cfg + ) # Initialize results array. dts_augmented: NDArrayFloat = np.zeros((N, T + E + 1)) @@ -154,18 +158,247 @@ def accumulate( if is_evaluated_dts.sum() > 0 and is_evaluated_gts.sum() > 0: # Compute true positives by assigning detections and ground truths. - dts_assignments, gts_assignments = assign(dts[is_evaluated_dts], gts[is_evaluated_gts], cfg) + dts_assignments, gts_assignments = assign( + dts[is_evaluated_dts], gts[is_evaluated_gts], cfg + ) dts_augmented[is_evaluated_dts, :-1] = dts_assignments gts_augmented[is_evaluated_gts, :-1] = gts_assignments # Permute the detections according to the original ordering. - outputs: Tuple[NDArrayInt, NDArrayInt] = np.unique(permutation, return_index=True) # type: ignore + outputs: Tuple[NDArrayInt, NDArrayInt] = np.unique(permutation, return_index=True) _, inverse_permutation = outputs dts_augmented = dts_augmented[inverse_permutation] return dts_augmented, gts_augmented -def assign(dts: NDArrayFloat, gts: NDArrayFloat, cfg: DetectionCfg) -> Tuple[NDArrayFloat, NDArrayFloat]: +def is_evaluated( + dts: NDArrayFloat, + gts: NDArrayFloat, + dts_cats: NDArrayObject, + gts_cats: NDArrayObject, + uuid: Tuple[str, int], + cfg: DetectionCfg, + avm: Optional[ArgoverseStaticMap] = None, + city_SE3_ego: Optional[SE3] = None, +) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayObject, NDArrayObject, Tuple[str, int]]: + """Filters detections and ground truth boxes that are either not within the max_range_m or within the ROI. + + Args: + dts: Detections array. + gts: Ground truth annotations array. + dts_cats: Categories associated with the detections array. + gts_cats: Categories associated with the ground truth annotations array. + uuid: List of unique identifiers (e.g. log_id:timestamp) + cfg: 3D object detection configuration. + avm: Argoverse static map for the log. + city_SE3_ego: Egovehicle pose in the city reference frame. + + Returns: + dts: Detections array. + gts: Ground truth annotations array. + dts_cats: Categories associated with the detections array. + gts_cats: Categories associated with the ground truth annotations array. + uuid: List of unique identifiers (e.g. log_id:timestamp) + """ + N, M = len(dts), len(gts) + is_evaluated_dts: NDArrayBool = np.ones(N, dtype=bool) + is_evaluated_gts: NDArrayBool = np.ones(M, dtype=bool) + + if avm is not None and city_SE3_ego is not None: + is_evaluated_dts &= compute_objects_in_roi_mask(dts, city_SE3_ego, avm) + is_evaluated_gts &= compute_objects_in_roi_mask(gts, city_SE3_ego, avm) + + is_evaluated_dts &= compute_evaluated_dts_mask(dts[..., :3], cfg) + is_evaluated_gts &= compute_evaluated_gts_mask( + gts[..., :3], gts[..., -1].astype(int), cfg + ) + + dts = dts[is_evaluated_dts] + gts = gts[is_evaluated_gts] + + dts_cats = dts_cats[is_evaluated_dts] + gts_cats = gts_cats[is_evaluated_gts] + return dts, gts, dts_cats, gts_cats, uuid + + +def filter_dont_care(gt: NDArrayObject, class_name: str) -> bool: + """Fitlers detections that are considered don't care under current LCA evaluation.""" + if gt == "ignore": + return True + + if gt == class_name: + return True + + else: + return False + + +def accumulate_hierarchy( + dts: NDArrayFloat, + gts: NDArrayFloat, + dts_cats: NDArrayObject, + gts_cats: NDArrayObject, + dts_uuids: NDArrayObject, + gts_uuids: NDArrayObject, + cat: str, + lca_cat: Tuple[str, ...], + lca: str, + cfg: DetectionCfg, +) -> Tuple[float, str, str]: + """Computes hierarchical AP at LCA=lca for each the given class (cat). + + Args: + dts: Detections array. + gts: Ground truth annotations array. + dts_cats: Categories associated with the detections array. + gts_cats: Categories associated with the ground truth annotations array. + dts_uuids: List of unique identifiers (e.g. log_id:timestamp) + gts_uuids: List of unique identifiers (e.g. log_id:timestamp) + cat: Category + lca_cat: Superclass of cat + lca: Least Common Ancestor, LCA={0,1,2} + cfg: 3D object detection configuration. + + Returns: + Hierarchical AP, cat, lca + """ + keep_dts = np.array([True if cname == cat else False for cname in dts_cats]) + keep_gts = np.array([True if cname in lca_cat else False for cname in gts_cats]) + + dts = dts[keep_dts] + gts = gts[keep_gts] + dts_cats = dts_cats[keep_dts] + gts_cats = gts_cats[keep_gts] + dts_uuids = dts_uuids[keep_dts] + gts_uuids = gts_uuids[keep_gts] + + scores = dts[..., -1] + permutation = np.argsort(-scores).tolist() + dts = dts[permutation] + dts_cats = dts_cats[permutation] + dts_uuids = dts_uuids[permutation] + + dist_mat = -compute_affinity_matrix(dts[..., :3], gts[..., :3], cfg.affinity_type) + + npos = sum([True if cname == cat else False for cname in gts_cats]) + + tp: Dict[int, Any] = {} + fp: Dict[int, Any] = {} + gt_name: Dict[int, List[Any]] = {} + pred_name: Dict[int, List[Any]] = {} + taken: Dict[int, Set[Tuple[Any, Any, Any]]] = {} + for i in range(len(cfg.affinity_thresholds_m)): + tp[i] = [] + fp[i] = [] + gt_name[i] = [] + pred_name[i] = [] + taken[i] = set() + + for pred_idx, pred in enumerate(zip(dts, dts_cats, dts_uuids)): + pred_box, pred_cat, pred_uuid = pred + min_dist = len(cfg.affinity_thresholds_m) * [np.inf] + match_gt_idx = len(cfg.affinity_thresholds_m) * [None] + + if len(gts_uuids) > 0: + keep_sweep = np.all( + gts_uuids == np.array([gts.shape[0] * [pred_uuid]]).squeeze(), axis=1 + ) + else: + keep_sweep = [] + + gt_ind_sweep = np.arange(gts.shape[0])[keep_sweep] + gts_sweep = gts[keep_sweep] + gts_cats_sweep = gts_cats[keep_sweep] + gts_uuids_sweep = gts_uuids[keep_sweep] + + for gt in zip(gt_ind_sweep, gts_sweep, gts_cats_sweep, gts_uuids_sweep): + gt_idx, gt_box, gt_cat, gt_uuid = gt + + # Find closest match among ground truth boxes + for i in range(len(cfg.affinity_thresholds_m)): + if ( + gt_cat == cat + and not (pred_uuid[0], pred_uuid[1], gt_idx) in taken[i] + ): + this_distance = dist_mat[pred_idx][gt_idx] + if this_distance < min_dist[i]: + min_dist[i] = this_distance + match_gt_idx[i] = gt_idx + + is_match = [ + min_dist[i] < dist_th for i, dist_th in enumerate(cfg.affinity_thresholds_m) + ] + + for gt in zip(gt_ind_sweep, gts_sweep, gts_cats_sweep, gts_uuids_sweep): + gt_idx, gt_box, gt_cat, gt_uuid = gt + # Find closest match among ground truth boxes + + for i in range(len(cfg.affinity_thresholds_m)): + if ( + not is_match[i] + and not (pred_uuid[0], pred_uuid[1], gt_idx) in taken[i] + ): + this_distance = dist_mat[pred_idx][gt_idx] + if this_distance < min_dist[i]: + min_dist[i] = this_distance + match_gt_idx[i] = gt_idx + + is_dist = [ + min_dist[i] < dist_th for i, dist_th in enumerate(cfg.affinity_thresholds_m) + ] + is_match = [ + True if is_dist[i] and gts_cats[match_gt_idx[i]] == cat else False + for i in range(len(cfg.affinity_thresholds_m)) + ] + + for i in range(len(cfg.affinity_thresholds_m)): + if is_match[i]: + taken[i].add((pred_uuid[0], pred_uuid[1], gt_idx)) + tp[i].append(1) + fp[i].append(0) + + gt_name[i].append(gts_cats[match_gt_idx[i]]) + pred_name[i].append(pred_cat) + else: + tp[i].append(0) + fp[i].append(1) + + if is_dist[i]: + gt_name[i].append(gts_cats[match_gt_idx[i]]) + else: + gt_name[i].append("ignore") + + pred_name[i].append(pred_cat) + + mAP = [] + for i in range(len(cfg.affinity_thresholds_m)): + select = [filter_dont_care(gt, cat) for gt in gt_name[i]] + + tp[i] = np.array(tp[i])[select] + fp[i] = np.array(fp[i])[select] + + if len(tp[i]) == 0: + return 0.0, cat, lca + + tp[i] = np.cumsum(tp[i]).astype(float) + fp[i] = np.cumsum(fp[i]).astype(float) + + prec = tp[i] / (fp[i] + tp[i]) + rec = tp[i] / float(npos) + + rec_interp = np.linspace( + 0, 1, NUM_RECALL_SAMPLES + ) # 101 steps, from 0% to 100% recall. + ap = np.mean(np.interp(rec_interp, rec, prec, right=0)) + + mAP.append(round(ap, NUM_DECIMALS)) + + return float(np.mean(mAP)), cat, lca + + +def assign( + dts: NDArrayFloat, gts: NDArrayFloat, cfg: DetectionCfg +) -> Tuple[NDArrayFloat, NDArrayFloat]: """Attempt assignment of each detection to a ground truth label. The detections (gts) and ground truth annotations (gts) are expected to be shape (N,10) and (M,10) @@ -190,18 +423,22 @@ def assign(dts: NDArrayFloat, gts: NDArrayFloat, cfg: DetectionCfg) -> Tuple[NDA $$T$$: cfg.affinity_thresholds_m (0.5, 1.0, 2.0, 4.0 by default). $$E$$: ATE, ASE, AOE. """ - affinity_matrix = compute_affinity_matrix(dts[..., :3], gts[..., :3], cfg.affinity_type) + affinity_matrix = compute_affinity_matrix( + dts[..., :3], gts[..., :3], cfg.affinity_type + ) # Get the GT label for each max-affinity GT label, detection pair. - idx_gts = affinity_matrix.argmax(axis=1)[None] + idx_gts: NDArrayInt = affinity_matrix.argmax(axis=1)[None] # The affinity matrix is an N by M matrix of the detections and ground truth labels respectively. # We want to take the corresponding affinity for each of the initial assignments using `gt_matches`. # The following line grabs the max affinity for each detection to a ground truth label. - affinities: NDArrayFloat = np.take_along_axis(affinity_matrix.transpose(), idx_gts, axis=0)[0] # type: ignore + affinities: NDArrayFloat = np.take_along_axis( + affinity_matrix.transpose(), idx_gts, axis=0 + )[0] # Find the indices of the _first_ detection assigned to each GT. - assignments: Tuple[NDArrayInt, NDArrayInt] = np.unique(idx_gts, return_index=True) # type: ignore + assignments: Tuple[NDArrayInt, NDArrayInt] = np.unique(idx_gts, return_index=True) idx_gts, idx_dts = assignments T, E = len(cfg.affinity_thresholds_m), 3 @@ -226,14 +463,22 @@ def assign(dts: NDArrayFloat, gts: NDArrayFloat, cfg: DetectionCfg) -> Tuple[NDA tps_dts = dts[idx_tps_dts] tps_gts = gts[idx_tps_gts] - translation_errors = distance(tps_dts[:, :3], tps_gts[:, :3], DistanceType.TRANSLATION) + translation_errors = distance( + tps_dts[:, :3], tps_gts[:, :3], DistanceType.TRANSLATION + ) scale_errors = distance(tps_dts[:, 3:6], tps_gts[:, 3:6], DistanceType.SCALE) - orientation_errors = distance(tps_dts[:, 6:10], tps_gts[:, 6:10], DistanceType.ORIENTATION) - dts_metrics[idx_tps_dts, 4:] = np.stack((translation_errors, scale_errors, orientation_errors), axis=-1) + orientation_errors = distance( + tps_dts[:, 6:10], tps_gts[:, 6:10], DistanceType.ORIENTATION + ) + dts_metrics[idx_tps_dts, 4:] = np.stack( + (translation_errors, scale_errors, orientation_errors), axis=-1 + ) return dts_metrics, gts_metrics -def interpolate_precision(precision: NDArrayFloat, interpolation_method: InterpType = InterpType.ALL) -> NDArrayFloat: +def interpolate_precision( + precision: NDArrayFloat, interpolation_method: InterpType = InterpType.ALL +) -> NDArrayFloat: r"""Interpolate the precision at each sampled recall. This function smooths the precision-recall curve according to the method introduced in Pascal @@ -263,7 +508,9 @@ def interpolate_precision(precision: NDArrayFloat, interpolation_method: InterpT return precision_interpolated -def compute_affinity_matrix(dts: NDArrayFloat, gts: NDArrayFloat, metric: AffinityType) -> NDArrayFloat: +def compute_affinity_matrix( + dts: NDArrayFloat, gts: NDArrayFloat, metric: AffinityType +) -> NDArrayFloat: """Calculate the affinity matrix between detections and ground truth annotations. Args: @@ -313,13 +560,17 @@ def compute_average_precision( precision = interpolate_precision(precision) # Evaluate precision at different recalls. - precision_interpolated: NDArrayFloat = np.interp(recall_interpolated, recall, precision, right=0) # type: ignore + precision_interpolated: NDArrayFloat = np.interp( + recall_interpolated, recall, precision, right=0 + ) - average_precision: float = np.mean(precision_interpolated) + average_precision = np.mean(precision_interpolated).astype(float) return average_precision, precision_interpolated -def distance(dts: NDArrayFloat, gts: NDArrayFloat, metric: DistanceType) -> NDArrayFloat: +def distance( + dts: NDArrayFloat, gts: NDArrayFloat, metric: DistanceType +) -> NDArrayFloat: """Distance functions between detections and ground truth. Args: @@ -334,7 +585,7 @@ def distance(dts: NDArrayFloat, gts: NDArrayFloat, metric: DistanceType) -> NDAr NotImplementedError: If the distance type is not supported. """ if metric == DistanceType.TRANSLATION: - translation_errors: NDArrayFloat = np.linalg.norm(dts - gts, axis=1) # type: ignore + translation_errors: NDArrayFloat = np.linalg.norm(dts - gts, axis=1) return translation_errors elif metric == DistanceType.SCALE: scale_errors: NDArrayFloat = 1 - iou_3d_axis_aligned(dts, gts) @@ -348,7 +599,9 @@ def distance(dts: NDArrayFloat, gts: NDArrayFloat, metric: DistanceType) -> NDAr raise NotImplementedError("This distance metric is not implemented!") -def compute_objects_in_roi_mask(cuboids_ego: NDArrayFloat, city_SE3_ego: SE3, avm: ArgoverseStaticMap) -> NDArrayBool: +def compute_objects_in_roi_mask( + cuboids_ego: NDArrayFloat, city_SE3_ego: SE3, avm: ArgoverseStaticMap +) -> NDArrayBool: """Compute the evaluated cuboids mask based off whether _any_ of their vertices fall into the ROI. Args: @@ -363,7 +616,9 @@ def compute_objects_in_roi_mask(cuboids_ego: NDArrayFloat, city_SE3_ego: SE3, av if len(cuboids_ego) == 0: is_within_roi = np.zeros((0,), dtype=bool) return is_within_roi - cuboid_list_ego: CuboidList = CuboidList([Cuboid.from_numpy(params) for params in cuboids_ego]) + cuboid_list_ego: CuboidList = CuboidList( + [Cuboid.from_numpy(params) for params in cuboids_ego] + ) cuboid_list_city = cuboid_list_ego.transform(city_SE3_ego) cuboid_list_vertices_m_city = cuboid_list_city.vertices_m @@ -397,14 +652,14 @@ def compute_evaluated_dts_mask( if len(xyz_m_ego) == 0: is_evaluated = np.zeros((0,), dtype=bool) return is_evaluated - norm: NDArrayFloat = np.linalg.norm(xyz_m_ego, axis=1) # type: ignore + norm: NDArrayFloat = np.linalg.norm(xyz_m_ego, axis=1) is_evaluated = norm < cfg.max_range_m cumsum: NDArrayInt = np.cumsum(is_evaluated) max_idx_arr: NDArrayInt = np.where(cumsum > cfg.max_num_dts_per_category)[0] if len(max_idx_arr) > 0: max_idx = max_idx_arr[0] - is_evaluated[max_idx:] = False # type: ignore + is_evaluated[max_idx:] = False return is_evaluated @@ -431,13 +686,13 @@ def compute_evaluated_gts_mask( if len(xyz_m_ego) == 0: is_evaluated = np.zeros((0,), dtype=bool) return is_evaluated - norm: NDArrayFloat = np.linalg.norm(xyz_m_ego, axis=1) # type: ignore + norm: NDArrayFloat = np.linalg.norm(xyz_m_ego, axis=1) is_evaluated = np.logical_and(norm < cfg.max_range_m, num_interior_pts > 0) return is_evaluated def load_mapped_avm_and_egoposes( - log_ids: List[str], dataset_dir: Path + log_ids: List[str], dataset_dir: Union[Path, UPath] ) -> Tuple[Dict[str, ArgoverseStaticMap], Dict[str, TimestampedCitySE3EgoPoses]]: """Load the maps and egoposes for each log in the dataset directory. @@ -451,28 +706,17 @@ def load_mapped_avm_and_egoposes( Raises: RuntimeError: If the process for loading maps and timestamped egoposes fails. """ - log_id_to_timestamped_poses = {log_id: read_city_SE3_ego(dataset_dir / log_id) for log_id in log_ids} - avms: Optional[List[ArgoverseStaticMap]] = Parallel(n_jobs=-1, backend="threading", verbose=1)( - delayed(ArgoverseStaticMap.from_map_dir)(dataset_dir / log_id / "map", build_raster=True) for log_id in log_ids + log_id_to_timestamped_poses = { + log_id: read_city_SE3_ego(dataset_dir / log_id) for log_id in log_ids + } + avms: Optional[List[ArgoverseStaticMap]] = Parallel(n_jobs=-1, backend="threading")( + delayed(ArgoverseStaticMap.from_map_dir)( + dataset_dir / log_id / "map", build_raster=True + ) + for log_id in log_ids ) + if avms is None: raise RuntimeError("Map and egopose loading has failed!") log_id_to_avm = {log_ids[i]: avm for i, avm in enumerate(avms)} return log_id_to_avm, log_id_to_timestamped_poses - - -def groupby(names: List[str], values: NDArrayFloat) -> Dict[str, NDArrayFloat]: - """Group a set of values by their corresponding names. - - Args: - names: String which maps data to a "bin". - values: Data which will be grouped by their names. - - Returns: - Dictionary mapping the group name to the corresponding group. - """ - outputs: Tuple[NDArrayInt, NDArrayInt] = np.unique(names, return_index=True) # type: ignore - unique_items, unique_items_indices = outputs - dts_groups: List[NDArrayFloat] = np.split(values, unique_items_indices[1:]) # type: ignore - uuid_to_groups = {unique_items[i]: x for i, x in enumerate(dts_groups)} - return uuid_to_groups diff --git a/src/av2/evaluation/forecasting/__init__.py b/src/av2/evaluation/forecasting/__init__.py new file mode 100644 index 00000000..eedc3941 --- /dev/null +++ b/src/av2/evaluation/forecasting/__init__.py @@ -0,0 +1 @@ +"""Forecasting evaluation sub-package.""" diff --git a/src/av2/evaluation/forecasting/constants.py b/src/av2/evaluation/forecasting/constants.py new file mode 100644 index 00000000..e5a7bb7c --- /dev/null +++ b/src/av2/evaluation/forecasting/constants.py @@ -0,0 +1,47 @@ +"""Constants for the forecasting challenge.""" + +from typing import Final + +import numpy as np + +from av2.evaluation import SensorCompetitionCategories + +NUM_TIMESTEPS: Final = 6 + +AV2_CATEGORIES: Final = tuple(x.value for x in SensorCompetitionCategories) +CATEGORY_TO_VELOCITY_M_PER_S: Final = { + "ARTICULATED_BUS": 4.58, + "BICYCLE": 0.97, + "BICYCLIST": 3.61, + "BOLLARD": 0.02, + "BOX_TRUCK": 2.59, + "BUS": 3.10, + "CONSTRUCTION_BARREL": 0.03, + "CONSTRUCTION_CONE": 0.02, + "DOG": 0.72, + "LARGE_VEHICLE": 1.56, + "MESSAGE_BOARD_TRAILER": 0.41, + "MOBILE_PEDESTRIAN_CROSSING_SIGN": 0.03, + "MOTORCYCLE": 1.58, + "MOTORCYCLIST": 4.08, + "PEDESTRIAN": 0.80, + "REGULAR_VEHICLE": 2.36, + "SCHOOL_BUS": 4.44, + "SIGN": 0.05, + "STOP_SIGN": 0.09, + "STROLLER": 0.91, + "TRUCK": 2.76, + "TRUCK_CAB": 2.36, + "VEHICULAR_TRAILER": 1.72, + "WHEELCHAIR": 1.50, + "WHEELED_DEVICE": 0.37, + "WHEELED_RIDER": 2.03, +} + +DISTANCE_THRESHOLDS_M: Final = (0.5, 1, 2, 4) +FORECAST_SCALAR: Final = np.linspace(0, 1, NUM_TIMESTEPS + 1) +MAX_DISPLACEMENT_M: Final = 50 +NUM_DECIMALS: Final = 3 +TIME_DELTA_S: Final = 0.5 +TP_THRESHOLD_M: Final = 2 +VELOCITY_TYPES: Final = ("static", "linear", "non-linear") diff --git a/src/av2/evaluation/forecasting/eval.py b/src/av2/evaluation/forecasting/eval.py new file mode 100644 index 00000000..c33cbefd --- /dev/null +++ b/src/av2/evaluation/forecasting/eval.py @@ -0,0 +1,529 @@ +"""Argoverse Forecasting evaluation. + +Evaluation Metrics: + mAP: see https://arxiv.org/abs/2203.16297 +""" + +import itertools +import json +import pickle +from collections import defaultdict +from pathlib import Path +from pprint import pprint +from typing import Any, Dict, List, Optional, Tuple, cast + +import click +import numpy as np +from scipy.spatial.transform import Rotation +from tqdm import tqdm + +from av2.evaluation import NUM_RECALL_SAMPLES +from av2.evaluation.detection.utils import ( + compute_objects_in_roi_mask, + load_mapped_avm_and_egoposes, +) +from av2.evaluation.forecasting import constants, utils +from av2.evaluation.forecasting.constants import ( + AV2_CATEGORIES, + CATEGORY_TO_VELOCITY_M_PER_S, + DISTANCE_THRESHOLDS_M, + MAX_DISPLACEMENT_M, + VELOCITY_TYPES, +) +from av2.utils.typing import NDArrayFloat + +from ..typing import ForecastSequences, Sequences + + +def evaluate( + predictions: ForecastSequences, + raw_ground_truth: Sequences, + top_k: int, + max_range_m: int, + dataset_dir: Optional[str], +) -> Dict[str, Any]: + """Compute Mean Forecasting AP, ADE, and FDE given predicted and ground truth forecasts. + + Args: + predictions: All predicted trajectories for each log_id and timestep. + raw_ground_truth: All ground truth trajectories for each log_id and timestep. + top_k: Top K evaluation. + max_range_m: Maximum evaluation range. + dataset_dir: Path to dataset. Required for ROI pruning. + + Returns: + Dictionary of evaluation results. + """ + ground_truth: ForecastSequences = convert_forecast_labels(raw_ground_truth) + ground_truth = filter_max_dist(ground_truth, max_range_m) + + utils.annotate_frame_metadata(predictions, ground_truth, ["ego_translation_m"]) + predictions = filter_max_dist(predictions, max_range_m) + + if dataset_dir is not None: + ground_truth = filter_drivable_area(ground_truth, dataset_dir) + predictions = filter_drivable_area(predictions, dataset_dir) + + results: Dict[str, Any] = {} + for velocity_type in VELOCITY_TYPES: + results[velocity_type] = {} + for category in AV2_CATEGORIES: + results[velocity_type][category] = {"mAP_F": [], "ADE": [], "FDE": []} + + gt_agents = [] + pred_agents = [] + for seq_id in tqdm(ground_truth.keys()): + for timestamp_ns in ground_truth[seq_id].keys(): + gt = [agent for agent in ground_truth[seq_id][timestamp_ns]] + + if seq_id in predictions and timestamp_ns in predictions[seq_id]: + pred = [agent for agent in predictions[seq_id][timestamp_ns]] + else: + pred = [] + + for agent in gt: + if agent["future_translation_m"].shape[0] < 1: + continue + + agent["seq_id"] = seq_id + agent["timestamp_ns"] = timestamp_ns + agent["velocity_m_per_s"] = utils.agent_velocity_m_per_s(agent) + agent["trajectory_type"] = utils.trajectory_type( + agent, CATEGORY_TO_VELOCITY_M_PER_S + ) + + gt_agents.append(agent) + + for agent in pred: + agent["seq_id"] = seq_id + agent["timestamp_ns"] = timestamp_ns + agent["velocity_m_per_s"] = utils.agent_velocity_m_per_s(agent) + agent["trajectory_type"] = utils.trajectory_type( + agent, CATEGORY_TO_VELOCITY_M_PER_S + ) + + pred_agents.append(agent) + + outputs = [] + for category, velocity_type, th in tqdm( + list(itertools.product(AV2_CATEGORIES, VELOCITY_TYPES, DISTANCE_THRESHOLDS_M)) + ): + category_velocity_m_per_s = CATEGORY_TO_VELOCITY_M_PER_S[category] + outputs.append( + accumulate( + pred_agents, + gt_agents, + top_k, + category, + velocity_type, + category_velocity_m_per_s, + th, + ) + ) + + for apf, ade, fde, category, velocity_type, threshold in outputs: + results[velocity_type][category]["mAP_F"].append(apf) + + if threshold == constants.TP_THRESHOLD_M: + results[velocity_type][category]["ADE"].append(ade) + results[velocity_type][category]["FDE"].append(fde) + + for category in AV2_CATEGORIES: + for velocity_type in VELOCITY_TYPES: + results[velocity_type][category]["mAP_F"] = round( + np.mean(results[velocity_type][category]["mAP_F"]), + constants.NUM_DECIMALS, + ) + results[velocity_type][category]["ADE"] = round( + np.mean(results[velocity_type][category]["ADE"]), constants.NUM_DECIMALS + ) + results[velocity_type][category]["FDE"] = round( + np.mean(results[velocity_type][category]["FDE"]), constants.NUM_DECIMALS + ) + + return results + + +def accumulate( + pred_agents: List[Dict[str, Any]], + gt_agents: List[Dict[str, Any]], + top_k: int, + class_name: str, + profile: str, + velocity: float, + threshold: float, +) -> Tuple[Any, Any, Any, str, str, float]: + """Perform matching between predicted and ground truth trajectories. + + Args: + pred_agents: List of predicted trajectories for a given log_id and timestamp_ns. + gt_agents: List of ground truth trajectories for a given log_id and timestamp_ns. + top_k: Number of future trajectories to consider when evaluating Forecastin AP, ADE and FDE (K=5 by default). + class_name: Match class name (e.g. car, pedestrian, bicycle) to determine if a trajectory is included + in evaluation. + profile: Match profile (e.g. static/linear/non-linear) to determine if a trajectory is included in evaluation. + velocity: Average velocity for each class. This determines whether a trajectory is static/linear/non-linear + threshold: Match threshold to determine true positives and false positives. + + Returns: + apf: Forecasting AP. + ade: Average displacement error. + fde: Final displacement error. + class_name: Match class name (e.g. car, pedestrian, bicycle) to determine if a trajectory is included + in evaluation. + profile: Match profile (e.g. static/linear/non-linear) to determine if a trajectory is included in evaluation. + """ + + def match(gt: str, pred: str, profile: str) -> bool: + return gt == profile or gt == "ignore" and pred == profile + + pred = [agent for agent in pred_agents if agent["name"] == class_name] + gt = [ + agent + for agent in gt_agents + if agent["name"] == class_name and agent["trajectory_type"] == profile + ] + conf = [agent["detection_score"] for agent in pred] + sortind = [i for (v, i) in sorted((v, i) for (i, v) in enumerate(conf))][::-1] + gt_agents_by_frame = defaultdict(list) + for agent in gt: + gt_agents_by_frame[(agent["seq_id"], agent["timestamp_ns"])].append(agent) + + npos = len(gt) + # --------------------------------------------- + # Match and accumulate match data. + # --------------------------------------------- + + gt_profiles, pred_profiles, agent_ade, agent_fde, tp, fp = [], [], [], [], [], [] + taken = set() # Initially no gt bounding box is matched. + for ind in sortind: + pred_agent = pred[ind] + min_dist = np.inf + match_gt_idx = None + + gt_agents_in_frame = gt_agents_by_frame[ + (pred_agent["seq_id"], pred_agent["timestamp_ns"]) + ] + for gt_idx, gt_agent in enumerate(gt_agents_in_frame): + if not (pred_agent["seq_id"], pred_agent["timestamp_ns"], gt_idx) in taken: + # Find closest match among ground truth boxes + this_distance = utils.center_distance( + gt_agent["current_translation_m"], + pred_agent["current_translation_m"], + ) + if this_distance < min_dist: + min_dist = this_distance + match_gt_idx = gt_idx + + # If the closest match is close enough according to threshold we have a match! + is_match = min_dist < threshold + + if is_match and match_gt_idx is not None: + taken.add((pred_agent["seq_id"], pred_agent["timestamp_ns"], match_gt_idx)) + gt_match_agent = gt_agents_in_frame[match_gt_idx] + + gt_len = gt_match_agent["future_translation_m"].shape[0] + forecast_match_th = [ + threshold + constants.FORECAST_SCALAR[i] * velocity + for i in range(gt_len + 1) + ] + + if top_k == 1: + ind = cast(int, np.argmax(pred_agent["score"])) + forecast_dist = [ + utils.center_distance( + gt_match_agent["future_translation_m"][i], + pred_agent["prediction_m"][ind][i], + ) + for i in range(gt_len) + ] + forecast_match = [ + dist < th for dist, th in zip(forecast_dist, forecast_match_th[1:]) + ] + + ade = cast(float, np.mean(forecast_dist)) + fde = forecast_dist[-1] + + elif top_k == 5: + forecast_dist, forecast_match = None, [False] + ade, fde = np.inf, np.inf + + for ind in range(top_k): + curr_forecast_dist = [ + utils.center_distance( + gt_match_agent["future_translation_m"][i], + pred_agent["prediction_m"][ind][i], + ) + for i in range(gt_len) + ] + curr_forecast_match = [ + dist < th + for dist, th in zip(curr_forecast_dist, forecast_match_th[1:]) + ] + + curr_ade = cast(float, np.mean(curr_forecast_dist)) + curr_fde = curr_forecast_dist[-1] + + if curr_ade < ade: + forecast_dist = curr_forecast_dist + forecast_match = curr_forecast_match + ade = curr_ade + fde = curr_fde + + agent_ade.append(ade) + agent_fde.append(fde) + tp.append(forecast_match[-1]) + fp.append(not forecast_match[-1]) + + gt_profiles.append(profile) + pred_profiles.append("ignore") + + else: + tp.append(False) + fp.append(True) + + ind = cast(int, np.argmax(pred_agent["score"])) + gt_profiles.append("ignore") + pred_profiles.append(pred_agent["trajectory_type"][ind]) + + select = [match(gt, pred, profile) for gt, pred in zip(gt_profiles, pred_profiles)] + tp_array = np.array(tp)[select] + fp_array = np.array(fp)[select] + + if len(gt) == 0: + return (np.nan, np.nan, np.nan, class_name, profile, threshold) + + if sum(tp_array) == 0: + return ( + cast(float, 0), + cast(float, MAX_DISPLACEMENT_M), + cast(float, MAX_DISPLACEMENT_M), + class_name, + profile, + threshold, + ) + + tp_array = np.cumsum(tp_array).astype(float) + fp_array = np.cumsum(fp_array).astype(float) + + prec = tp_array / (fp_array + tp_array) + rec = tp_array / float(npos) + + rec_interp = np.linspace( + 0, 1, NUM_RECALL_SAMPLES + ) # 101 steps, from 0% to 100% recall. + apf = np.mean(np.interp(rec_interp, rec, prec, right=0)) + + return ( + cast(float, apf), + min(cast(float, np.mean(agent_ade)), MAX_DISPLACEMENT_M), + min(cast(float, np.mean(agent_fde)), MAX_DISPLACEMENT_M), + class_name, + profile, + threshold, + ) + + +def convert_forecast_labels(labels: Any) -> Any: + """Convert the unified label format to a format that is easier to work with for forecasting evaluation. + + Args: + labels: Dictionary of labels. + + Returns: + forecast_labels, dictionary of labels in the forecasting format. + """ + forecast_labels = {} + for seq_id, frames in labels.items(): + frame_dict = {} + for frame_idx, frame in enumerate(frames): + forecast_instances = [] + for instance in utils.array_dict_iterator( + frame, len(frame["translation_m"]) + ): + future_translations: Any = [] + for future_frame in frames[ + frame_idx + 1 : frame_idx + 1 + constants.NUM_TIMESTEPS + ]: + if instance["track_id"] not in future_frame["track_id"]: + break + future_translations.append( + future_frame["translation_m"][ + future_frame["track_id"] == instance["track_id"] + ][0] + ) + + if len(future_translations) == 0: + continue + + forecast_instances.append( + { + "current_translation_m": instance["translation_m"][:2], + "ego_translation_m": instance["ego_translation_m"][:2], + "future_translation_m": np.array(future_translations)[:, :2], + "name": instance["name"], + "size": instance["size"], + "yaw": instance["yaw"], + "velocity_m_per_s": instance["velocity_m_per_s"][:2], + "label": instance["label"], + } + ) + if forecast_instances: + frame_dict[frame["timestamp_ns"]] = forecast_instances + + forecast_labels[seq_id] = frame_dict + + return forecast_labels + + +def filter_max_dist( + forecasts: ForecastSequences, max_range_m: int +) -> ForecastSequences: + """Remove all tracks that are beyond `max_range_m`. + + Args: + forecasts: Dictionary of tracks. + max_range_m: maximum distance from ego-vehicle. + + Returns: + Dictionary of tracks. + """ + for seq_id in forecasts.keys(): + for timestamp_ns in forecasts[seq_id].keys(): + keep_forecasts = [ + agent + for agent in forecasts[seq_id][timestamp_ns] + if "ego_translation_m" in agent + and np.linalg.norm( + agent["current_translation_m"] - agent["ego_translation_m"] + ) + < max_range_m + ] + forecasts[seq_id][timestamp_ns] = keep_forecasts + + return forecasts + + +def yaw_to_quaternion3d(yaw: float) -> NDArrayFloat: + """Convert a rotation angle in the xy plane (i.e. about the z axis) to a quaternion. + + Args: + yaw: angle to rotate about the z-axis, representing an Euler angle, in radians + Returns: + array w/ quaternion coefficients (qw,qx,qy,qz) in scalar-first order, per Argoverse convention. + """ + qx, qy, qz, qw = Rotation.from_euler(seq="z", angles=yaw, degrees=False).as_quat() + return np.array([qw, qx, qy, qz]) + + +def filter_drivable_area( + forecasts: ForecastSequences, dataset_dir: str +) -> ForecastSequences: + """Convert the unified label format to a format that is easier to work with for forecasting evaluation. + + Args: + forecasts: Dictionary of tracks. + dataset_dir: Dataset root directory. + + Returns: + forecasts: Dictionary of tracks. + """ + log_ids = list(forecasts.keys()) + log_id_to_avm, log_id_to_timestamped_poses = load_mapped_avm_and_egoposes( + log_ids, Path(dataset_dir) + ) + + for log_id in log_ids: + avm = log_id_to_avm[log_id] + + for timestamp_ns in forecasts[log_id]: + city_SE3_ego = log_id_to_timestamped_poses[log_id][int(timestamp_ns)] + + translation_m, size, quat = [], [], [] + + if len(forecasts[log_id][timestamp_ns]) == 0: + continue + + for box in forecasts[log_id][timestamp_ns]: + translation_m.append( + box["current_translation_m"] - box["ego_translation_m"] + ) + size.append(box["size"]) + quat.append(yaw_to_quaternion3d(box["yaw"])) + + score = np.ones((len(translation_m), 1)) + boxes = np.concatenate( + [ + np.array(translation_m), + np.array(size), + np.array(quat), + np.array(score), + ], + axis=1, + ) + + is_evaluated = compute_objects_in_roi_mask(boxes, city_SE3_ego, avm) + forecasts[log_id][timestamp_ns] = list( + np.array(forecasts[log_id][timestamp_ns])[is_evaluated] + ) + + return forecasts + + +@click.command() +@click.option("--predictions", required=True, help="Predictions PKL file") +@click.option("--ground_truth", required=True, help="Ground Truth PKL file") +@click.option("--max_range_m", default=50, type=int, help="Max evaluation range") +@click.option("--top_k", default=5, type=int, help="Top K evaluation metric") +@click.option( + "--dataset_dir", + default=None, + help="Path to dataset split (e.g. /data/Sensor/val). Required for ROI pruning", +) +@click.option("--out", required=True, help="Output PKL file") +def runner( + predictions: str, + ground_truth: str, + max_range_m: int, + dataset_dir: Any, + top_k: int, + out: str, +) -> None: + """Standalone evaluation function.""" + predictions2 = pickle.load(open(predictions, "rb")) + ground_truth2 = pickle.load(open(ground_truth, "rb")) + + res = evaluate(predictions2, ground_truth2, top_k, max_range_m, dataset_dir) + + mAP_F = np.nanmean( + [ + metrics["mAP_F"] + for traj_metrics in res.values() + for metrics in traj_metrics.values() + ] + ) + ADE = np.nanmean( + [ + metrics["ADE"] + for traj_metrics in res.values() + for metrics in traj_metrics.values() + ] + ) + FDE = np.nanmean( + [ + metrics["FDE"] + for traj_metrics in res.values() + for metrics in traj_metrics.values() + ] + ) + res["mean_mAP_F"] = mAP_F + res["mean_ADE"] = ADE + res["mean_FDE"] = FDE + pprint(res) + + with open(out, "w") as f: + json.dump(res, f, indent=4) + + +if __name__ == "__main__": + runner() diff --git a/src/av2/evaluation/forecasting/utils.py b/src/av2/evaluation/forecasting/utils.py new file mode 100644 index 00000000..61d912ce --- /dev/null +++ b/src/av2/evaluation/forecasting/utils.py @@ -0,0 +1,173 @@ +"""Forecasting evaluation utilities.""" + +from typing import Any, Dict, Iterable, List, Union, cast + +import numpy as np + +from av2.evaluation.forecasting import constants +from av2.utils.typing import NDArrayFloat, NDArrayInt + +from ..typing import ForecastSequences, Frame + + +def agent_velocity_m_per_s(agent: Dict[str, Any]) -> NDArrayFloat: + """Get the agent velocity_m_per_s. + + velocity_m_per_s calculated as the delta between the current translation_m and the translation_m in the next timestep. + + Args: + agent: Dictionary containing the agent's translations + + Returns: + Ground truth velocity_m_per_s for label agents or + List of velocities corresponding to each movement prediction for predicted agents + """ + if "future_translation_m" in agent: # ground_truth + return cast( + NDArrayFloat, + (agent["future_translation_m"][0][:2] - agent["current_translation_m"][:2]) + / constants.TIME_DELTA_S, + ) + + else: # predictions + res = [] + for i in range(agent["prediction_m"].shape[0]): + res.append( + (agent["prediction_m"][i][0][:2] - agent["current_translation_m"][:2]) + / constants.TIME_DELTA_S + ) + + return np.stack(res) + + +def trajectory_type( + agent: Dict[str, Any], category_velocity_m_per_s: Dict[str, float] +) -> Union[str, List[str]]: + """Get the trajectory type, which is either static, linear or non-linear. + + Trajectory is static if the prediction error of a static forecast is below threshold. + Trajectory is linear if the prediction error of the linear forecast is below threshold. + Linear forecast is created by linearly extrapolating the current velocity_m_per_s. + Trajectory is non-linear if it is neither static or linear. + + Args: + agent: Dictionary containing the agent's translations and velocity_m_per_s. + category_velocity_m_per_s: Average velocity_m_per_s of each class, used to determine the prediction error threshold for each category. + + Returns: + String corresponding to the trajectory type for agents in the ground-truth annotations or + List of strings, one trajectory for each movement prediction for predicted agents + """ + if "future_translation_m" in agent: # ground_truth + time = agent["future_translation_m"].shape[0] * constants.TIME_DELTA_S + static_target = agent["current_translation_m"][:2] + linear_target = ( + agent["current_translation_m"][:2] + time * agent["velocity_m_per_s"][:2] + ) + + final_position = agent["future_translation_m"][-1][:2] + + threshold = 1 + constants.FORECAST_SCALAR[ + len(agent["future_translation_m"]) + ] * category_velocity_m_per_s.get(agent["name"], 0) + if np.linalg.norm(final_position - static_target) < threshold: + return "static" + elif np.linalg.norm(final_position - linear_target) < threshold: + return "linear" + else: + return "non-linear" + + else: # predictions + res: List[str] = [] + time = agent["prediction_m"].shape[1] * constants.TIME_DELTA_S + + threshold = 1 + constants.FORECAST_SCALAR[ + len(agent["prediction_m"]) + ] * category_velocity_m_per_s.get(agent["name"], 0) + for i in range(agent["prediction_m"].shape[0]): + static_target = agent["current_translation_m"][:2] + linear_target = ( + agent["current_translation_m"][:2] + + time * agent["velocity_m_per_s"][i][:2] + ) + + final_position = agent["prediction_m"][i][-1][:2] + + if np.linalg.norm(final_position - static_target) < threshold: + res.append("static") + elif np.linalg.norm(final_position - linear_target) < threshold: + res.append("linear") + else: + res.append("non-linear") + + return res + + +def center_distance(pred_box: NDArrayFloat, gt_box: NDArrayFloat) -> float: + """Get euclidean distance between two centers. + + Args: + pred_box: center 1 + gt_box: center 2 + + Returns: + distance between two centers + """ + return cast(float, np.linalg.norm(pred_box - gt_box)) + + +def array_dict_iterator(array_dict: Frame, length: int) -> Iterable[Frame]: + """Get an iterator over each index in array_dict. + + Args: + array_dict: dictionary of numpy arrays + length: number of elements to iterate over + + Returns: + Iterator, each element is a dictionary of numpy arrays, indexed from 0 to (length-1) + """ + return (index_array_values(array_dict, i) for i in range(length)) + + +def index_array_values(array_dict: Frame, index: Union[int, NDArrayInt]) -> Frame: + """Index each numpy array in dictionary. + + Args: + array_dict: dictionary of numpy arrays + index: index used to access each numpy array in array_dict + + Returns: + Dictionary of numpy arrays, each indexed by the provided index + """ + return { + k: v[index] if isinstance(v, np.ndarray) else v for k, v in array_dict.items() + } + + +def annotate_frame_metadata( + predictions: ForecastSequences, + ground_truth: ForecastSequences, + metadata_keys: List[str], +) -> ForecastSequences: + """Index each numpy array in dictionary. + + Args: + predictions: Forecast predictions + ground_truth: Forecast ground truth + metadata_keys : Ground truth keys to copy to predictions + + Returns: + predictions: Forecast predictions with new metadat key + """ + for seq_id in ground_truth.keys(): + for timestamp_ns in ground_truth[seq_id].keys(): + copy_keys = {} + + for key in metadata_keys: + copy_keys[key] = ground_truth[seq_id][timestamp_ns][0][key] + + for i in range(len(predictions[seq_id][timestamp_ns])): + for key in metadata_keys: + predictions[seq_id][timestamp_ns][i][key] = copy_keys[key] + + return predictions diff --git a/src/av2/evaluation/scene_flow/constants.py b/src/av2/evaluation/scene_flow/constants.py new file mode 100644 index 00000000..d6e57cdf --- /dev/null +++ b/src/av2/evaluation/scene_flow/constants.py @@ -0,0 +1,116 @@ +"""Constants for scene flow evaluation.""" + +from __future__ import annotations + +from enum import Enum, unique +from typing import Final + +from av2.datasets.sensor.constants import AnnotationCategories + +SCENE_FLOW_DYNAMIC_THRESHOLD: Final = 0.05 +SWEEP_PAIR_TIME_DELTA: Final = 0.1 + +CATEGORY_TO_INDEX: Final = { + **{"NONE": 0}, + **{k.value: i + 1 for i, k in enumerate(AnnotationCategories)}, +} + + +@unique +class SceneFlowMetricType(str, Enum): + """Scene Flow metrics.""" + + ACCURACY_RELAX = "ACCURACY_RELAX" + ACCURACY_STRICT = "ACCURACY_STRICT" + ANGLE_ERROR = "ANGLE_ERROR" + EPE = "EPE" + + +@unique +class SegmentationMetricType(str, Enum): + """Segmentation metrics.""" + + TP = "TP" + TN = "TN" + FP = "FP" + FN = "FN" + + +@unique +class InanimateCategories(str, Enum): + """Annotation categories representing inanimate objects that aren't vehicles.""" + + BOLLARD = "BOLLARD" + CONSTRUCTION_BARREL = "CONSTRUCTION_BARREL" + CONSTRUCTION_CONE = "CONSTRUCTION_CONE" + MOBILE_PEDESTRIAN_CROSSING_SIGN = "MOBILE_PEDESTRIAN_CROSSING_SIGN" + SIGN = "SIGN" + STOP_SIGN = "STOP_SIGN" + + +@unique +class LeggedCategories(str, Enum): + """Annotation categories representing objects that move using legs.""" + + ANIMAL = "ANIMAL" + DOG = "DOG" + OFFICIAL_SIGNALER = "OFFICIAL_SIGNALER" + PEDESTRIAN = "PEDESTRIAN" + + +@unique +class SmallVehicleCategories(str, Enum): + """Annotation categories representing small vehicles.""" + + BICYCLE = "BICYCLE" + BICYCLIST = "BICYCLIST" + MOTORCYCLE = "MOTORCYCLE" + MOTORCYCLIST = "MOTORCYCLIST" + STROLLER = "STROLLER" + WHEELCHAIR = "WHEELCHAIR" + WHEELED_DEVICE = "WHEELED_DEVICE" + WHEELED_RIDER = "WHEELED_RIDER" + + +@unique +class VehicleCategories(str, Enum): + """Annotation categories representing regular vehicles.""" + + ARTICULATED_BUS = "ARTICULATED_BUS" + BOX_TRUCK = "BOX_TRUCK" + BUS = "BUS" + LARGE_VEHICLE = "LARGE_VEHICLE" + MESSAGE_BOARD_TRAILER = "MESSAGE_BOARD_TRAILER" + RAILED_VEHICLE = "RAILED_VEHICLE" + REGULAR_VEHICLE = "REGULAR_VEHICLE" + SCHOOL_BUS = "SCHOOL_BUS" + TRAFFIC_LIGHT_TRAILER = "TRAFFIC_LIGHT_TRAILER" + TRUCK = "TRUCK" + TRUCK_CAB = "TRUCK_CAB" + VEHICULAR_TRAILER = "VEHICULAR_TRAILER" + + +@unique +class MetricBreakdownCategories(str, Enum): + """Meta-categories for the scene flow task.""" + + ALL = "All" + BACKGROUND = "Background" + FOREGROUND = "Foreground" + + +NO_CLASS_BREAKDOWN: Final = {MetricBreakdownCategories.ALL: list(range(31))} +FOREGROUND_BACKGROUND_BREAKDOWN: Final = { + MetricBreakdownCategories.BACKGROUND: [0], + MetricBreakdownCategories.FOREGROUND: [ + CATEGORY_TO_INDEX[k.value] + for k in ( + list(InanimateCategories) + + list(LeggedCategories) + + list(SmallVehicleCategories) + + list(VehicleCategories) + ) + ], +} + +FLOW_COLUMNS: Final = ("flow_tx_m", "flow_ty_m", "flow_tz_m") diff --git a/src/av2/evaluation/scene_flow/eval.py b/src/av2/evaluation/scene_flow/eval.py new file mode 100644 index 00000000..a9496135 --- /dev/null +++ b/src/av2/evaluation/scene_flow/eval.py @@ -0,0 +1,542 @@ +"""Argoverse 2 Scene Flow Evaluation.""" + +from __future__ import annotations + +import zipfile +from collections import defaultdict +from pathlib import Path +from typing import ( + Any, + Callable, + DefaultDict, + Dict, + Final, + List, + Optional, + Tuple, + Union, + cast, +) +from zipfile import ZipFile + +import click +import numpy as np +import pandas as pd +from rich.progress import track + +import av2.evaluation.scene_flow.constants as constants +from av2.evaluation.scene_flow.constants import ( + SceneFlowMetricType, + SegmentationMetricType, +) +from av2.utils.typing import NDArrayBool, NDArrayFloat, NDArrayInt + +ACCURACY_RELAX_DISTANCE_THRESHOLD: Final = 0.1 +ACCURACY_STRICT_DISTANCE_THRESHOLD: Final = 0.05 +NO_FMT_INDICES: Final = ("Background", "Dynamic") +EPS: Final = 1e-10 + + +def compute_end_point_error(dts: NDArrayFloat, gts: NDArrayFloat) -> NDArrayFloat: + """Compute the end-point error between predictions and ground truth. + + Args: + dts: (N,3) Array containing predicted flows. + gts: (N,3) Array containing ground truth flows. + + Returns: + The point-wise end-point error. + """ + end_point_error: NDArrayFloat = np.linalg.norm(dts - gts, axis=-1).astype( + np.float64 + ) + return end_point_error + + +def compute_accuracy( + dts: NDArrayFloat, gts: NDArrayFloat, distance_threshold: float +) -> NDArrayFloat: + """Compute the percent of inliers for a given threshold for a set of prediction and ground truth vectors. + + Args: + dts: (N,3) Array containing predicted flows. + gts: (N,3) Array containing ground truth flows. + distance_threshold: Distance threshold for classifying inliers. + + Returns: + The pointwise inlier assignments. + """ + l2_norm = np.linalg.norm(dts - gts, axis=-1) + gts_norm = np.linalg.norm(gts, axis=-1) + relative_error = np.divide(l2_norm, gts_norm + EPS) + abs_error_inlier = np.less(l2_norm, distance_threshold).astype(bool) + relative_error_inlier = np.less(relative_error, distance_threshold).astype(bool) + accuracy: NDArrayFloat = np.logical_or( + abs_error_inlier, relative_error_inlier + ).astype(np.float64) + return accuracy + + +def compute_accuracy_strict(dts: NDArrayFloat, gts: NDArrayFloat) -> NDArrayFloat: + """Compute the accuracy with a 0.05 threshold. + + Args: + dts: (N,3) Array containing predicted flows. + gts: (N,3) Array containing ground truth flows. + + Returns: + The pointwise inlier assignments at a 0.05 threshold + """ + return compute_accuracy(dts, gts, ACCURACY_STRICT_DISTANCE_THRESHOLD) + + +def compute_accuracy_relax(dts: NDArrayFloat, gts: NDArrayFloat) -> NDArrayFloat: + """Compute the accuracy with a 0.1 threshold. + + Args: + dts: (N,3) Array containing predicted flows. + gts: (N,3) Array containing ground truth flows. + + Returns: + The pointwise inlier assignments at a 0.1 threshold. + """ + return compute_accuracy(dts, gts, ACCURACY_RELAX_DISTANCE_THRESHOLD) + + +def compute_angle_error(dts: NDArrayFloat, gts: NDArrayFloat) -> NDArrayFloat: + """Compute the angle error in space-time between the prediced and ground truth flow vectors. + + Args: + dts: (N,3) Array containing predicted flows. + gts: (N,3) Array containing ground truth flows. + + Returns: + The pointwise angle errors in space-time. + """ + # Convert the 3D flow vectors to 4D space-time vectors. + dts_space_time = np.pad( + dts, ((0, 0), (0, 1)), constant_values=constants.SWEEP_PAIR_TIME_DELTA + ) + gts_space_time = np.pad( + gts, ((0, 0), (0, 1)), constant_values=constants.SWEEP_PAIR_TIME_DELTA + ) + + dts_space_time_norm = np.linalg.norm(dts_space_time, axis=-1, keepdims=True) + gts_space_time_norm = np.linalg.norm(gts_space_time, axis=-1, keepdims=True) + unit_dts = dts_space_time / dts_space_time_norm + unit_gts = gts_space_time / gts_space_time_norm + + dot_product = np.einsum("bd,bd->b", unit_dts, unit_gts) + + # Floating point errors can cause `dot_product` to be slightly greater than 1 or less than -1. + clipped_dot_product = np.clip(dot_product, -1.0, 1.0) + angle_error: NDArrayFloat = np.arccos(clipped_dot_product).astype(np.float64) + return angle_error + + +def compute_true_positives(dts: NDArrayBool, gts: NDArrayBool) -> int: + """Compute true positive count. + + Args: + dts: (N,) Array containing predicted dynamic segmentation. + gts: (N,) Array containing ground truth dynamic segmentation. + + Returns: + The number of true positive classifications. + """ + return int(np.logical_and(dts, gts).sum()) + + +def compute_true_negatives(dts: NDArrayBool, gts: NDArrayBool) -> int: + """Compute true negative count. + + Args: + dts: (N,) Array containing predicted dynamic segmentation. + gts: (N,) Array containing ground truth dynamic segmentation. + + Returns: + The number of true negative classifications. + """ + return int(np.logical_and(~dts, ~gts).sum()) + + +def compute_false_positives(dts: NDArrayBool, gts: NDArrayBool) -> int: + """Compute false positive count. + + Args: + dts: (N,) Array containing predicted dynamic segmentation. + gts: (N,) Array containing ground truth dynamic segmentation. + + Returns: + The number of false positive classifications. + """ + return int(np.logical_and(dts, ~gts).sum()) + + +def compute_false_negatives(dts: NDArrayBool, gts: NDArrayBool) -> int: + """Compute false negative count. + + Args: + dts: (N,) Array containing predicted dynamic segmentation. + gts: (N,) Array containing ground truth dynamic segmentation. + + Returns: + The number of false negative classifications + """ + return int(np.logical_and(~dts, gts).sum()) + + +def compute_scene_flow_metrics( + dts: NDArrayFloat, gts: NDArrayFloat, scene_flow_metric_type: SceneFlowMetricType +) -> NDArrayFloat: + """Compute scene flow metrics. + + Args: + dts: (N,3) Array containing predicted flows. + gts: (N,3) Array containing ground truth flows. + scene_flow_metric_type: Scene flow metric type. + + Returns: + Scene flow metric corresponding to `scene_flow_metric_type`. + + Raises: + NotImplementedError: If the `scene_flow_metric_type` is not implemented. + """ + if scene_flow_metric_type == SceneFlowMetricType.ACCURACY_RELAX: + return compute_accuracy_relax(dts, gts) + elif scene_flow_metric_type == SceneFlowMetricType.ACCURACY_STRICT: + return compute_accuracy_strict(dts, gts) + elif scene_flow_metric_type == SceneFlowMetricType.ANGLE_ERROR: + return compute_angle_error(dts, gts) + elif scene_flow_metric_type == SceneFlowMetricType.EPE: + return compute_end_point_error(dts, gts) + else: + raise NotImplementedError( + f"The scene flow metric type {scene_flow_metric_type} is not implemented!" + ) + + +def compute_segmentation_metrics( + dts: NDArrayBool, gts: NDArrayBool, segmentation_metric_type: SegmentationMetricType +) -> int: + """Compute segmentation metrics. + + Args: + dts: (N,) Array containing predicted dynamic segmentation. + gts: (N,) Array containing ground truth dynamic segmentation. + segmentation_metric_type: Segmentation metric type. + + Returns: + Segmentation metric corresponding to `segmentation_metric_type`. + + Raises: + NotImplementedError: If the `segmentation_metric_type` is not implemented. + """ + if segmentation_metric_type == SegmentationMetricType.TP: + return compute_true_positives(dts, gts) + elif segmentation_metric_type == SegmentationMetricType.TN: + return compute_true_negatives(dts, gts) + elif segmentation_metric_type == SegmentationMetricType.FP: + return compute_false_positives(dts, gts) + elif segmentation_metric_type == SegmentationMetricType.FN: + return compute_false_negatives(dts, gts) + else: + raise NotImplementedError( + f"The segmentation metric type {segmentation_metric_type} is not implemented!" + ) + + +def compute_metrics( + pred_flow: NDArrayFloat, + pred_dynamic: NDArrayBool, + gts: NDArrayFloat, + category_indices: NDArrayInt, + is_dynamic: NDArrayBool, + is_close: NDArrayBool, + is_valid: NDArrayBool, + metric_categories: Dict[constants.MetricBreakdownCategories, List[int]], +) -> Dict[str, List[Any]]: + """Compute all the metrics for a given example and package them into a list to be put into a DataFrame. + + Args: + pred_flow: (N,3) Predicted flow vectors. + pred_dynamic: (N,) Predicted dynamic labels. + gts: (N,3) Ground truth flow vectors. + category_indices: (N,) Integer class labels for each point. + is_dynamic: (N,) Ground truth dynamic labels. + is_close: (N,) True for a point if it is within a 70m x 70m box around the AV. + is_valid: (N,) True for a point if its flow vector was successfully computed. + metric_categories: A dictionary mapping segmentation labels to groups of category indices. + + Returns: + A dictionary of columns to create a long-form DataFrame of the results from. + One row for each subset in the breakdown. + """ + pred_flow = pred_flow[is_valid].astype(np.float64) + pred_dynamic = pred_dynamic[is_valid].astype(bool) + gts = gts[is_valid].astype(np.float64) + category_indices = category_indices[is_valid].astype(int) + is_dynamic = is_dynamic[is_valid].astype(bool) + is_close = is_close[is_valid].astype(bool) + + results: DefaultDict[str, List[Any]] = defaultdict(list) + + # Each metric is broken down by point labels on Object Class, Motion, and Distance from the AV. + # We iterate over all combinations of those three categories and compute average metrics on each subset. + for cls, category_idxs in metric_categories.items(): + # Compute the union of all masks within the meta-category. + category_mask = category_indices == category_idxs[0] + for i in category_idxs[1:]: + category_mask = np.logical_or(category_mask, (category_indices == i)) + + for motion, m_mask in [("Dynamic", is_dynamic), ("Static", ~is_dynamic)]: + for distance, d_mask in [("Close", is_close), ("Far", ~is_close)]: + mask = category_mask & m_mask & d_mask + subset_size = mask.sum().item() + gts_sub = gts[mask] + pred_sub = pred_flow[mask] + results["Class"] += [cls.value] + results["Motion"] += [motion] + results["Distance"] += [distance] + results["Count"] += [subset_size] + + # Check if there are any points in this subset and if so compute all the average metrics. + if subset_size > 0: + for flow_metric_type in SceneFlowMetricType: + results[flow_metric_type] += [ + compute_scene_flow_metrics( + pred_sub, gts_sub, flow_metric_type + ).mean() + ] + for seg_metric_type in SegmentationMetricType: + results[seg_metric_type] += [ + compute_segmentation_metrics( + pred_dynamic[mask], is_dynamic[mask], seg_metric_type + ) + ] + else: + for flow_metric_type in SceneFlowMetricType: + results[flow_metric_type] += [np.nan] + for seg_metric_type in SegmentationMetricType: + results[seg_metric_type] += [0.0] + return results + + +def evaluate_predictions( + annotations_dir: Path, get_prediction: Callable[[Path], pd.DataFrame] +) -> pd.DataFrame: + """Run the evaluation on predictions and labels. + + Args: + annotations_dir: Path to the directory containing the annotation files produced by `make_annotation_files.py`. + get_prediction: Function that retrieves a predictions DataFrame for a given relative + annotation filepath, or None if no prediction exists. + + Returns: + DataFrame containing the average metrics on each subset of each example. + """ + results: DefaultDict[str, List[Any]] = defaultdict(list) + annotation_files = sorted(annotations_dir.rglob("*.feather")) + for anno_file in track(annotation_files, description="Evaluating..."): + gts = pd.read_feather(anno_file) + name: Path = anno_file.relative_to(annotations_dir) + pred = get_prediction(name) + if pred is None: + continue + current_example_results = compute_metrics( + pred[list(constants.FLOW_COLUMNS)].to_numpy().astype(float), + pred["is_dynamic"].to_numpy().astype(bool), + gts[list(constants.FLOW_COLUMNS)].to_numpy().astype(float), + gts["category_indices"].to_numpy().astype(np.uint8), + gts["is_dynamic"].to_numpy().astype(bool), + gts["is_close"].to_numpy().astype(bool), + gts["is_valid"].to_numpy().astype(bool), + constants.FOREGROUND_BACKGROUND_BREAKDOWN, + ) + num_subsets = len(list(current_example_results.values())[0]) + results["Example"] += [str(name) for _ in range(num_subsets)] + for m in current_example_results: + results[m] += current_example_results[m] + df = pd.DataFrame( + results, + columns=["Example", "Class", "Motion", "Distance", "Count"] + + list(SceneFlowMetricType) + + list(SegmentationMetricType), + ) + return df + + +def get_prediction_from_directory( + annotation_name: Path, predictions_dir: Path +) -> Optional[pd.DataFrame]: + """Get the prediction corresponding annotation from a directory of prediction files. + + Args: + annotation_name: Relative path to the annotation file. + predictions_dir: Path to the predicition files in submission_format. + + Returns: + DataFrame containing the predictions for that annotation file or None if it does not exist. + """ + pred_file = predictions_dir / annotation_name + if not pred_file.exists(): + return None + pred = pd.read_feather(pred_file) + return pred + + +def get_prediction_from_zipfile( + annotation_name: Path, predictions_zip: Path +) -> Optional[pd.DataFrame]: + """Get the prediction corresponding annotation from a zip archive of prediction files. + + Args: + annotation_name: Relative path to the annotation file. + predictions_zip: Path to the prediction files in a zip archive. + + Returns: + DataFrame containing the predictions for that annotation file or None if it does not exist. + """ + with ZipFile(predictions_zip, "r") as zf: + name = annotation_name.as_posix() + path = zipfile.Path(zf, name) + if path.exists(): + return pd.read_feather(zf.open(name)) + else: + return None + + +def evaluate_directories(annotations_dir: Path, predictions_dir: Path) -> pd.DataFrame: + """Run the evaluation on predictions and labels saved to disk. + + Args: + annotations_dir: Path to the directory containing the annotation files produced by `make_annotation_files.py`. + predictions_dir: Path to the prediction files in submission format. + + Returns: + DataFrame containing the average metrics on each subset of each example. + """ + return evaluate_predictions( + annotations_dir, lambda n: get_prediction_from_directory(n, predictions_dir) + ) + + +def evaluate_zip(annotations_dir: Path, predictions_zip: Path) -> pd.DataFrame: + """Run the evaluation on predictions and labels saved to disk. + + Args: + annotations_dir: Path to the directory containing the annotation files produced by `make_annotation_files.py`. + predictions_zip: Path to the prediction files in a zip archive. + + Returns: + DataFrame containing the average metrics on each subset of each example. + """ + return evaluate_predictions( + annotations_dir, lambda n: get_prediction_from_zipfile(n, predictions_zip) + ) + + +def results_to_dict(frame: pd.DataFrame) -> Dict[str, float]: + """Convert a results DataFrame to a dictionary of whole dataset metrics. + + Args: + frame: DataFrame returned by evaluate_directories. + + Returns: + Dictionary string keys "" mapped to average metrics on that subset. + """ + output = {} + grouped = frame.groupby(["Class", "Motion", "Distance"]) + + def weighted_average( + x: pd.DataFrame, metric_type: Union[SceneFlowMetricType, SegmentationMetricType] + ) -> float: + """Weighted average of metric m using the Count column. + + Args: + x: Input data-frame. + metric_type: Metric type. + + Returns: + Weighted average over the metric_type; + """ + total = cast(int, x["Count"].sum()) + if total == 0: + return np.nan + averages: float = (x[metric_type.value] * x.Count).sum() / total + return averages + + for metric_type in SceneFlowMetricType: + avg: pd.Series[float] = grouped.apply( + lambda x, m=metric_type: weighted_average(x, metric_type=m) + ) + segments: List[Tuple[str, str, str]] = avg.index.to_list() + for segment in segments: + if segment[:2] == NO_FMT_INDICES: + continue + + metric_type_str = ( + metric_type.title().replace("_", " ") + if metric_type != SceneFlowMetricType.EPE + else metric_type + ) + name = metric_type_str + "/" + "/".join([str(i) for i in segment]) + output[name] = avg.loc[segment] + + grouped = frame.groupby(["Class", "Motion"]) + for metric_type in SceneFlowMetricType: + avg_nodist: pd.Series[float] = grouped.apply( + lambda x, m=metric_type: weighted_average(x, metric_type=m) + ) + segments_nodist: List[Tuple[str, str, str]] = avg_nodist.index.to_list() + for segment in segments_nodist: + if segment[:2] == NO_FMT_INDICES: + continue + + metric_type_str = ( + metric_type.title().replace("_", " ") + if metric_type != SceneFlowMetricType.EPE + else metric_type + ) + name = metric_type_str + "/" + "/".join([str(i) for i in segment]) + output[name] = avg_nodist.loc[segment] + output["Dynamic IoU"] = frame.TP.sum() / ( + frame.TP.sum() + frame.FP.sum() + frame.FN.sum() + ) + output["EPE 3-Way Average"] = ( + output["EPE/Foreground/Dynamic"] + + output["EPE/Foreground/Static"] + + output["EPE/Background/Static"] + ) / 3 + return output + + +def evaluate(annotations_dir: str, predictions_dir: str) -> Dict[str, float]: + """Evaluate a set of predictions and print the results. + + Args: + annotations_dir: Path to the directory containing the annotation files produced by `make_annotation_files.py`. + predictions_dir: Path to the prediction files in submission format. + + Returns: + The results as a dict of metric names and values. + """ + results_df = evaluate_directories(Path(annotations_dir), Path(predictions_dir)) + results_dict = results_to_dict(results_df) + + for metric in sorted(results_dict): + print(f"{metric}: {results_dict[metric]:.3f}") + + return results_dict + + +@click.command() +@click.argument("annotations_dir", type=str) +@click.argument("predictions_dir", type=str) +def _evaluate_entry(annotations_dir: str, predictions_dir: str) -> Dict[str, float]: + """Entry point for evaluate.""" + return evaluate(annotations_dir, predictions_dir) + + +if __name__ == "__main__": + _evaluate_entry() diff --git a/src/av2/evaluation/scene_flow/example_submission.py b/src/av2/evaluation/scene_flow/example_submission.py new file mode 100644 index 00000000..17c3ca52 --- /dev/null +++ b/src/av2/evaluation/scene_flow/example_submission.py @@ -0,0 +1,65 @@ +"""An example showing how to output flow predictions in the format required for submission.""" + +from pathlib import Path + +import click +import numpy as np +from kornia.geometry.linalg import transform_points +from rich.progress import track + +from av2.evaluation.scene_flow.utils import ( + get_eval_point_mask, + get_eval_subset, + write_output_file, +) +from av2.torch.data_loaders.scene_flow import SceneFlowDataloader + + +def example_submission( + output_dir: str, mask_file: str, data_dir: str, name: str +) -> None: + """Output example submission files for the leaderboard. Predicts the ego motion for every point. + + Args: + output_dir: Path to output directory. + mask_file: Archive of submission masks. + data_dir: Path to input data. + name: Name of the dataset (e.g. av2). + """ + data_loader = SceneFlowDataloader(Path(data_dir), name, "test") + + output_root = Path(output_dir) + output_root.mkdir(exist_ok=True) + + eval_inds = get_eval_subset(data_loader) + for i in track(eval_inds, description="Generating outputs..."): + sweep_0, sweep_1, ego_1_SE3_ego_0, flow = data_loader[i] + mask = get_eval_point_mask(sweep_0.sweep_uuid, Path(mask_file)) + + pc1 = sweep_0.lidar.as_tensor()[mask, :3] + pc1_rigid = transform_points(ego_1_SE3_ego_0.matrix(), pc1[None])[0] + rigid_flow = (pc1_rigid - pc1).detach().numpy() + is_dynamic = np.zeros(len(rigid_flow), dtype=bool) + + write_output_file(rigid_flow, is_dynamic, sweep_0.sweep_uuid, output_root) + + +@click.command() +@click.argument("output_dir", type=str) +@click.argument("data_dir", type=str) +@click.argument("mask_file", type=str) +@click.option( + "--name", + type=str, + help="the data should be located in //sensor/", + default="av2", +) +def _example_submission_entry( + output_dir: str, mask_file: str, data_dir: str, name: str +) -> None: + """Entry point for example_submission.""" + example_submission(output_dir, mask_file, data_dir, name) + + +if __name__ == "__main__": + _example_submission_entry() diff --git a/src/av2/evaluation/scene_flow/make_annotation_files.py b/src/av2/evaluation/scene_flow/make_annotation_files.py new file mode 100644 index 00000000..85f2bd37 --- /dev/null +++ b/src/av2/evaluation/scene_flow/make_annotation_files.py @@ -0,0 +1,131 @@ +"""Utility program for producing minimnal annotation files used for evaluation on the val and test splits.""" + +from pathlib import Path +from typing import Final, Tuple + +import click +import numpy as np +import pandas as pd +from rich.progress import track + +from av2.evaluation.scene_flow.utils import get_eval_point_mask, get_eval_subset +from av2.torch.data_loaders.scene_flow import SceneFlowDataloader +from av2.utils.typing import NDArrayBool, NDArrayFloat, NDArrayInt + +CLOSE_DISTANCE_THRESHOLD: Final = 35.0 + + +def write_annotation( + category_indices: NDArrayInt, + is_close: NDArrayBool, + is_dynamic: NDArrayBool, + is_valid: NDArrayBool, + flow: NDArrayFloat, + sweep_uuid: Tuple[str, int], + output_dir: Path, +) -> None: + """Write an annotation file. + + Args: + category_indices: Category label indices. + is_close: Close (inside 70 meter box) labels. + is_dynamic: Dynamic labels. + is_valid: Valid flow labels. + flow: Flow labels. + sweep_uuid: Log id and timestamp_ns of the sweep. + output_dir: Top level directory to store the output in. + """ + output = pd.DataFrame( + { + "category_indices": category_indices.astype(np.uint8), + "is_close": is_close.astype(bool), + "is_dynamic": is_dynamic.astype(bool), + "is_valid": is_valid.astype(bool), + "flow_tx_m": flow[:, 0].astype(np.float16), + "flow_ty_m": flow[:, 1].astype(np.float16), + "flow_tz_m": flow[:, 2].astype(np.float16), + } + ) + + log_id, timestamp_ns = sweep_uuid + + output_subdir = output_dir / log_id + output_subdir.mkdir(exist_ok=True) + output_file = output_subdir / f"{timestamp_ns}.feather" + output.to_feather(output_file) + + +def make_annotation_files( + output_dir: str, mask_file: str, data_dir: str, name: str, split: str +) -> None: + """Create annotation files for running the evaluation. + + Args: + output_dir: Path to output directory. + data_dir: Path to input data. + mask_file: Archive of submission masks. + name: Name of the dataset (e.g. av2). + split: Split to make annotations for. + + Raises: + ValueError: If the dataset does not have annotations. + """ + data_loader = SceneFlowDataloader(Path(data_dir), name, "val") + + output_root = Path(output_dir) + output_root.mkdir(exist_ok=True) + + eval_inds = get_eval_subset(data_loader) + for i in track(eval_inds): + sweep_0, _, _, flow_labels = data_loader[i] + if flow_labels is None: + raise ValueError("Missing flow annotations!") + + mask = get_eval_point_mask(sweep_0.sweep_uuid, Path(mask_file)) + + flow = flow_labels.flow[mask].numpy().astype(np.float16) + is_valid = flow_labels.is_valid[mask].numpy().astype(bool) + category_indices = flow_labels.category_indices[mask].numpy().astype(np.uint8) + is_dynamic = flow_labels.is_dynamic[mask].numpy().astype(bool) + + pc = sweep_0.lidar.as_tensor()[mask, :3].numpy() + is_close = np.logical_and.reduce( + np.abs(pc[:, :2]) <= CLOSE_DISTANCE_THRESHOLD, axis=1 + ).astype(bool) + + write_annotation( + category_indices, + is_close, + is_dynamic, + is_valid, + flow, + sweep_0.sweep_uuid, + output_root, + ) + + +@click.command() +@click.argument("output_dir", type=str) +@click.argument("data_dir", type=str) +@click.argument("mask_file", type=str) +@click.option( + "--name", + type=str, + help="the data should be located in //sensor/", + default="av2", +) +@click.option( + "--split", + help="the data should be located in //sensor/", + default="val", + type=click.Choice(["test", "val"]), +) +def _make_annotation_files_entry( + output_dir: str, mask_file: str, data_dir: str, name: str, split: str +) -> None: + """Entry point for make_annotation_files.""" + make_annotation_files(output_dir, mask_file, data_dir, name, split) + + +if __name__ == "__main__": + _make_annotation_files_entry() diff --git a/src/av2/evaluation/scene_flow/make_mask_files.py b/src/av2/evaluation/scene_flow/make_mask_files.py new file mode 100644 index 00000000..6e9ac256 --- /dev/null +++ b/src/av2/evaluation/scene_flow/make_mask_files.py @@ -0,0 +1,80 @@ +"""Utility program for producing submission mask files.""" + +import zipfile +from pathlib import Path + +import click +import pandas as pd +from kornia.geometry.liegroup import Se3 +from rich.progress import track + +from av2.evaluation.scene_flow.utils import compute_eval_point_mask, get_eval_subset +from av2.torch.data_loaders.scene_flow import SceneFlowDataloader +from av2.torch.structures.sweep import Sweep + + +def get_mask( + s0: Sweep, + s1: Sweep, + s1_SE3_s0: Se3, +) -> pd.DataFrame: + """Get a mask packaged up and ready for writing to disk. + + Args: + s0: The first sweep of the pair. + s1: The second sweep of the pair. + s1_SE3_s0: The relative ego-motion between the two sweeps. + + Returns: + DataFrame with a single column for the mask. + """ + mask = compute_eval_point_mask((s0, s1, s1_SE3_s0, None)) + output = pd.DataFrame({"mask": mask.numpy().astype(bool)}) + return output + + +def make_mask_files(output_file: str, data_dir: str, name: str, split: str) -> None: + """Create an archive file of pointwise masks for submission to the leaderboard. + + Args: + output_file: Path to output file archive. + data_dir: Path to input data. + name: Name of the dataset (e.g. av2). + split: Split to make masks for. + """ + data_loader = SceneFlowDataloader(Path(data_dir), name, split) + eval_inds = get_eval_subset(data_loader) + with zipfile.ZipFile(Path(output_file), "w") as maskzip: + for i in track(eval_inds): + sweep_0, sweep_1, ego, _ = data_loader[i] + mask_df = get_mask(sweep_0, sweep_1, ego) + log, timestamp_ns = sweep_0.sweep_uuid + output_path = f"{log}/{timestamp_ns}.feather" + with maskzip.open(output_path, "w") as zip_output_file: + mask_df.to_feather(zip_output_file) + + +@click.command() +@click.argument("output_file", type=str) +@click.argument("data_dir", type=str) +@click.option( + "--name", + type=str, + help="the data should be located in //sensor/", + default="av2", +) +@click.option( + "--split", + help="the data should be located in //sensor/", + default="val", + type=click.Choice(["test", "val"]), +) +def _make_mask_files_entry( + output_file: str, data_dir: str, name: str, split: str +) -> None: + """Entry point for make_mask_files.""" + make_mask_files(output_file, data_dir, name, split) + + +if __name__ == "__main__": + _make_mask_files_entry() diff --git a/src/av2/evaluation/scene_flow/make_submission_archive.py b/src/av2/evaluation/scene_flow/make_submission_archive.py new file mode 100644 index 00000000..972bf7a2 --- /dev/null +++ b/src/av2/evaluation/scene_flow/make_submission_archive.py @@ -0,0 +1,120 @@ +"""Validate and package a set of prediction files for submission to the leaderboard.""" + +from pathlib import Path +from typing import Final +from zipfile import ZipFile + +import click +import numpy as np +import pandas as pd +from rich.progress import track + +SUBMISSION_COLUMNS: Final = ("flow_tx_m", "flow_ty_m", "flow_tz_m", "is_dynamic") + + +def validate(submission_dir: Path, mask_file: Path) -> None: + """Validate the filenames and shapes of all predictions required for submission. + + Args: + submission_dir: Path to the top level submission file directory. + mask_file: Archive containing all the mask files required for submission. + + Raises: + FileNotFoundError: If any of the required files are missing + ValueError: If any supplied file is malformed + """ + with ZipFile(mask_file, "r") as masks: + mask_files = [ + f.filename for f in masks.filelist if f.filename.endswith(".feather") + ] + for filename in track(mask_files, description="Validating..."): + input_file = submission_dir / filename + if not input_file.exists(): + raise FileNotFoundError( + f"{input_file} not found in submission directory" + ) + pred = pd.read_feather(input_file) + expected_num_points = pd.read_feather(masks.open(filename)).sum().item() + + for c in SUBMISSION_COLUMNS: + if c not in pred.columns: + raise ValueError(f"{input_file} does not contain {c}") + if c == "is_dynamic": + if pred[c].dtype != bool: + raise ValueError( + f"{input_file} column {c} should be bool but is {pred[c].dtype}" + ) + else: + if pred[c].dtype != np.float16: + raise ValueError( + f"{input_file} column {c} should be float16 but is {pred[c].dtype}" + ) + + if len(pred.columns) > 4: + raise ValueError(f"{input_file} contains extra columns") + + if len(pred) != expected_num_points: + raise ValueError( + f"{input_file} has {len(pred)} rows but it should have {expected_num_points}" + ) + + +def zip(submission_dir: Path, mask_file: Path, output_file: Path) -> None: + """Package all validated submission files into a zip archive. + + Args: + submission_dir: Path to the top level submission file directory. + mask_file: Archive containing all the mask files required for submission. + output_file: File to store the zip archive in. + """ + with ZipFile(mask_file, "r") as masks: + mask_files = [ + f.filename for f in masks.filelist if f.filename.endswith(".feather") + ] + with ZipFile(output_file, "w") as myzip: + for filename in track(mask_files, description="Zipping..."): + input_file = submission_dir / filename + myzip.write(input_file, arcname=filename) + + +def make_submission_archive( + submission_dir: str, mask_file: str, output_filename: str +) -> bool: + """Package prediction files into a zip archive for submission. + + Args: + submission_dir: Directory containing the prediction files to submit. + mask_file: Archive containing all the mask files required for submission. + output_filename: Name of the submission archive. + + Returns: + True if validation and zipping succeeded, False otherwise. + """ + output_file = Path(output_filename) + try: + validate(Path(submission_dir), Path(mask_file)) + except (FileNotFoundError, ValueError) as e: + print(f"Input validation failed with: {e}") + return False + + zip(Path(submission_dir), Path(mask_file), output_file) + return True + + +@click.command() +@click.argument("submission_dir", type=str) +@click.argument("mask_file", type=str) +@click.option( + "--output_filename", + type=str, + help="name of the output archive file", + default="submission.zip", +) +def _make_submission_archive_entry( + submission_dir: str, mask_file: str, output_filename: str +) -> bool: + return make_submission_archive(submission_dir, mask_file, output_filename) + + +if __name__ == "__main__": + _make_submission_archive_entry() diff --git a/src/av2/evaluation/scene_flow/utils.py b/src/av2/evaluation/scene_flow/utils.py new file mode 100644 index 00000000..27159144 --- /dev/null +++ b/src/av2/evaluation/scene_flow/utils.py @@ -0,0 +1,101 @@ +"""Utilities for generating output for the scene flow challenge.""" + +from pathlib import Path +from typing import Final, List, Optional, Tuple +from zipfile import ZipFile + +import numpy as np +import pandas as pd +import torch +from kornia.geometry.liegroup import Se3 +from torch import BoolTensor + +from av2.torch.data_loaders.scene_flow import SceneFlowDataloader +from av2.torch.structures.flow import Flow +from av2.torch.structures.sweep import Sweep +from av2.utils.typing import NDArrayBool, NDArrayFloat + +_EVAL_ROOT: Final = Path(__file__).resolve().parent + + +def get_eval_subset(dataloader: SceneFlowDataloader) -> List[int]: + """Return the indices of the test set used for evaluation on the leaderboard.""" + return list(range(len(dataloader)))[::5] + + +def get_eval_point_mask(sweep_uuid: Tuple[str, int], mask_file: Path) -> BoolTensor: + """Retrieve for a given sweep, a boolean mask indicating which points are evaluated on. + + Args: + sweep_uuid: The uuid of the first sweep in the pair to retrieve the mask for. + mask_file: Archive of submission masks. + + Returns: + The submission mask for that pair. + """ + with ZipFile(mask_file) as masks: + log_id, timestamp_ns = sweep_uuid + mask = ( + pd.read_feather(masks.open(f"{log_id}/{timestamp_ns}.feather")) + .to_numpy() + .astype(bool) + ) + + return BoolTensor(torch.from_numpy(mask).squeeze()) + + +def compute_eval_point_mask( + datum: Tuple[Sweep, Sweep, Se3, Optional[Flow]] +) -> BoolTensor: + """Compute for a given sweep, a boolean mask indicating which points are evaluated on. + + Note: This should NOT BE USED FOR CREATING SUBMISSIONS use get_eval_point_mask to ensure consistency. + + Args: + datum: Tuple returned from a `SceneFlowDataloader` to compute the mask for. + + Returns: + A mask indicating roughly which points will be evauated on. + + Raises: + ValueError: if datum does not have ground annotations. + """ + pcl = datum[0].lidar.as_tensor()[:, :3] + is_close = torch.logical_and( + (pcl[:, 0].abs() <= 50), (pcl[:, 1].abs() <= 50) + ).bool() + + if datum[0].is_ground is None: + raise ValueError("Must have ground annotations loaded to determine eval mask") + not_ground = torch.logical_not(datum[0].is_ground) + return BoolTensor(torch.logical_and(is_close, not_ground)) + + +def write_output_file( + flow: NDArrayFloat, + is_dynamic: NDArrayBool, + sweep_uuid: Tuple[str, int], + output_dir: Path, +) -> None: + """Write an output predictions file in the correct format for submission. + + Args: + flow: (N,3) Flow predictions. + is_dynamic: (N,) Dynamic segmentation prediction. + sweep_uuid: Identifier of the sweep being predicted (log_id, timestamp_ns). + output_dir: Top level directory containing all predictions. + """ + output_log_dir = output_dir / sweep_uuid[0] + output_log_dir.mkdir(exist_ok=True, parents=True) + fx_m = flow[:, 0].astype(np.float16) + fy_m = flow[:, 1].astype(np.float16) + fz_m = flow[:, 2].astype(np.float16) + output = pd.DataFrame( + { + "flow_tx_m": fx_m, + "flow_ty_m": fy_m, + "flow_tz_m": fz_m, + "is_dynamic": is_dynamic.astype(bool), + } + ) + output.to_feather(output_log_dir / f"{sweep_uuid[1]}.feather") diff --git a/src/av2/evaluation/tracking/__init__.py b/src/av2/evaluation/tracking/__init__.py new file mode 100644 index 00000000..c2868fc0 --- /dev/null +++ b/src/av2/evaluation/tracking/__init__.py @@ -0,0 +1 @@ +"""Tracking evaluation sub-package.""" diff --git a/src/av2/evaluation/tracking/constants.py b/src/av2/evaluation/tracking/constants.py new file mode 100644 index 00000000..418cf27a --- /dev/null +++ b/src/av2/evaluation/tracking/constants.py @@ -0,0 +1,12 @@ +"""Constants for tracking challenge.""" + +from typing import Final + +from av2.evaluation import SensorCompetitionCategories + +SUBMETRIC_TO_METRIC_CLASS_NAME: Final = { + "MOTA": "CLEAR", + "HOTA": "HOTA", +} + +AV2_CATEGORIES: Final = tuple(x.value for x in SensorCompetitionCategories) diff --git a/src/av2/evaluation/tracking/eval.py b/src/av2/evaluation/tracking/eval.py new file mode 100644 index 00000000..98124789 --- /dev/null +++ b/src/av2/evaluation/tracking/eval.py @@ -0,0 +1,659 @@ +"""Argoverse 2 Tracking evaluation. + +Evaluation Metrics: + HOTA: see https://arxiv.org/abs/2009.07736 + MOTA: see https://jivp-eurasipjournals.springeropen.com/articles/10.1155/2008/246309 + AMOTA: see https://arxiv.org/abs/2008.08063 +""" + +import contextlib +import json +import pickle +from copy import copy +from functools import partial +from itertools import chain +from pathlib import Path +from pprint import pprint +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union, cast + +import click +import numpy as np +import trackeval +from scipy.optimize import linear_sum_assignment +from scipy.spatial.transform import Rotation +from tqdm import tqdm +from trackeval.datasets._base_dataset import _BaseDataset + +from av2.evaluation.detection.utils import ( + compute_objects_in_roi_mask, + load_mapped_avm_and_egoposes, +) +from av2.evaluation.tracking import constants, utils +from av2.evaluation.tracking.constants import SUBMETRIC_TO_METRIC_CLASS_NAME +from av2.utils.typing import NDArrayFloat, NDArrayInt + +from ..typing import Sequences + + +class TrackEvalDataset(_BaseDataset): # type: ignore + """Dataset class to support tracking evaluation using the TrackEval library.""" + + def __init__(self, config: Dict[str, Any]) -> None: + """Store config.""" + super().__init__() + self.gt_tracks = config["GT_TRACKS"] + self.predicted_tracks = config["PREDICTED_TRACKS"] + self.full_class_list = config.get("CLASSES", config["CLASSES_TO_EVAL"]) + self.class_list = config["CLASSES_TO_EVAL"] + self.tracker_list = config["TRACKERS_TO_EVAL"] + self.seq_list = config["SEQ_IDS_TO_EVAL"] + self.output_fol = config["OUTPUT_FOLDER"] + self.output_sub_fol = config["OUTPUT_SUB_FOLDER"] + self.zero_distance = config["ZERO_DISTANCE"] + print(f"Using zero_distance={self.zero_distance}m") + + @staticmethod + def get_default_dataset_config() -> Dict[str, Any]: + """Get the default config. + + Returns: + dictionary of the default config + """ + default_config = { + "GT_TRACKS": None, # tracker_name -> seq id -> frames + "PREDICTED_TRACKS": None, # tracker_name -> seq id -> frames + "SEQ_IDS_TO_EVAL": None, # list of sequences ids to eval + "CLASSES_TO_EVAL": None, + "TRACKERS_TO_EVAL": None, + "OUTPUT_FOLDER": None, # Where to save eval results (if None, same as TRACKERS_FOLDER) + "OUTPUT_SUB_FOLDER": "", # Output files are saved in OUTPUT_FOLDER/tracker_name/OUTPUT_SUB_FOLDER + "ZERO_DISTANCE": 2, + } + return default_config + + def _load_raw_file( + self, tracker: str, seq_id: Union[str, int], is_gt: bool + ) -> Dict[str, Any]: + """Get raw track data, from either trackers or ground truth.""" + tracks = (self.gt_tracks if is_gt else self.predicted_tracks)[tracker][seq_id] + source = "gt" if is_gt else "tracker" + + ts = np.array([frame["timestamp_ns"] for frame in tracks]) + assert np.all(ts[:-1] < ts[1:]), "timestamps are not increasing" + + raw_data = { + f"{source}_ids": [frame["track_id"] for frame in tracks], + f"{source}_classes": [ + np.array([self.full_class_list.index(n) for n in frame["name"]]) + for frame in tracks + ], + f"{source}_dets": [ + np.concatenate((frame["translation_m"], frame["size"]), axis=-1) + for frame in tracks + ], + "num_timesteps": len(tracks), + "seq": seq_id, + } + if "score" in tracks[0]: + raw_data[f"{source}_confidences"] = [frame["score"] for frame in tracks] + return raw_data + + def get_preprocessed_seq_data( + self, raw_data: Dict[str, Any], cls: str + ) -> Dict[str, Any]: + """Filter data to keep only one class and map id to 0 - n. + + Args: + raw_data: dictionary of track data + cls: name of class to keep + + Returns: + Dictionary of processed track data of the specified class + """ + data_keys = [ + "gt_ids", + "tracker_ids", + "gt_classes", + "tracker_classes", + "gt_dets", + "tracker_dets", + "tracker_confidences", + "similarity_scores", + "num_timesteps", + "seq", + ] + data = {k: copy(raw_data[k]) for k in data_keys} + cls_id = self.full_class_list.index(cls) + + for t in range(raw_data["num_timesteps"]): + gt_to_keep_mask = data["gt_classes"][t] == cls_id + data["gt_classes"][t] = data["gt_classes"][t][gt_to_keep_mask] + data["gt_ids"][t] = data["gt_ids"][t][gt_to_keep_mask] + data["gt_dets"][t] = data["gt_dets"][t][gt_to_keep_mask, :] + + tracker_to_keep_mask = data["tracker_classes"][t] == cls_id + data["tracker_classes"][t] = data["tracker_classes"][t][ + tracker_to_keep_mask + ] + data["tracker_ids"][t] = data["tracker_ids"][t][tracker_to_keep_mask] + data["tracker_dets"][t] = data["tracker_dets"][t][tracker_to_keep_mask, :] + data["tracker_confidences"][t] = data["tracker_confidences"][t][ + tracker_to_keep_mask + ] + + data["similarity_scores"][t] = data["similarity_scores"][t][ + :, tracker_to_keep_mask + ][gt_to_keep_mask] + + # Map ids to 0 - n. + unique_gt_ids = set(chain.from_iterable(data["gt_ids"])) + unique_tracker_ids = set(chain.from_iterable(data["tracker_ids"])) + data["gt_ids"] = self._map_ids(data["gt_ids"], unique_gt_ids) + data["tracker_ids"] = self._map_ids(data["tracker_ids"], unique_tracker_ids) + + data["num_tracker_dets"] = sum(len(dets) for dets in data["tracker_dets"]) + data["num_gt_dets"] = sum(len(dets) for dets in data["gt_dets"]) + data["num_tracker_ids"] = len(unique_tracker_ids) + data["num_gt_ids"] = len(unique_gt_ids) + + # Ensure again that ids are unique per timestep after preproc. + self._check_unique_ids(data, after_preproc=True) + return data + + def _map_ids(self, ids: List[Any], unique_ids: Iterable[Any]) -> List[NDArrayInt]: + id_map = {id: i for i, id in enumerate(unique_ids)} + return [ + np.array([id_map[id] for id in id_array], dtype=int) for id_array in ids + ] + + def _calculate_similarities( + self, gt_dets_t: NDArrayFloat, tracker_dets_t: NDArrayFloat + ) -> NDArrayFloat: + """Euclidean distance of the x, y translation coordinates.""" + gt_xy = gt_dets_t[:, :2] + tracker_xy = tracker_dets_t[:, :2] + sim = self._calculate_euclidean_similarity( + gt_xy, tracker_xy, zero_distance=self.zero_distance + ) + return cast(NDArrayFloat, sim) + + +def evaluate_tracking( + labels: Sequences, + track_predictions: Sequences, + classes: List[str], + tracker_name: str, + output_dir: str, + iou_threshold: float = 0.5, +) -> Dict[str, Any]: + """Evaluate a set of tracks against ground truth annotations using the TrackEval evaluation suite. + + Each sequences/log is evaluated separately. + + Args: + labels: Dict[seq_id: List[frame]] Dictionary of ground truth annotations. + track_predictions: Dict[seq_id: List[frame]] Dictionary of tracks. + classes: List of classes to evaluate. + tracker_name: Name of tracker. + output_dir: Folder to save evaluation results. + iou_threshold: IoU threshold for a True Positive match between a detection to a ground truth bounding box. + + frame is a dictionary with the following format + { + sequences_id: [ + { + "timestamp_ns": int, # nano seconds + "track_id": np.ndarray[I], + "translation_m": np.ndarray[I, 3], + "size": np.ndarray[I, 3], + "yaw": np.ndarray[I], + "velocity_m_per_s": np.ndarray[I, 3], + "label": np.ndarray[I], + "score": np.ndarray[I], + "name": np.ndarray[I], + ... + } + ] + } + where I is the number of objects in the frame. + + Returns: + Dictionary of metric values. + """ + labels_id_ts = set( + (frame["seq_id"], frame["timestamp_ns"]) + for frame in utils.ungroup_frames(labels) + ) + predictions_id_ts = set( + (frame["seq_id"], frame["timestamp_ns"]) + for frame in utils.ungroup_frames(track_predictions) + ) + assert ( + labels_id_ts == predictions_id_ts + ), "sequences ids and timestamp_ns in labels and predictions don't match" + metrics_config = { + "METRICS": ["HOTA", "CLEAR"], + "THRESHOLD": iou_threshold, + } + metric_names = cast(List[str], metrics_config["METRICS"]) + metrics_list = [ + getattr(trackeval.metrics, metric)(metrics_config) for metric in metric_names + ] + dataset_config = { + **TrackEvalDataset.get_default_dataset_config(), + "GT_TRACKS": {tracker_name: labels}, + "PREDICTED_TRACKS": {tracker_name: track_predictions}, + "SEQ_IDS_TO_EVAL": list(labels.keys()), + "CLASSES_TO_EVAL": classes, + "TRACKERS_TO_EVAL": [tracker_name], + "OUTPUT_FOLDER": output_dir, + } + + evaluator = trackeval.Evaluator( + { + **trackeval.Evaluator.get_default_eval_config(), + "TIME_PROGRESS": False, + } + ) + full_result, _ = evaluator.evaluate( + [TrackEvalDataset(dataset_config)], + metrics_list, + ) + + return cast(Dict[str, Any], full_result) + + +def _tune_score_thresholds( + labels: Sequences, + track_predictions: Sequences, + objective_metric: str, + classes: List[str], + num_thresholds: int = 10, + iou_threshold: float = 0.5, + match_distance_m: int = 2, +) -> Tuple[Dict[str, float], Dict[str, float], Dict[str, float]]: + """Find the optimal score thresholds to optimize the objective metric. + + Each class is processed independently. + + Args: + labels: Dictionary of ground truth annotations + track_predictions: Dictionary of tracks + objective_metric: Name of the metric to optimize, one of HOTA or MOTA + classes: List of classes to evaluate + num_thresholds: Number of score thresholds to try + iou_threshold: IoU threshold for a True Positive match between a detection to a ground truth bounding box + match_distance_m: Maximum euclidean distance threshold for a match + + Returns: + optimal_score_threshold_by_class: Dictionary of class name to optimal score threshold + optimal_metric_values_by_class: Dictionary of class name to metric value with the optimal score threshold + mean_metric_values_by_class: Dictionary of class name to metric value averaged over recall levels + """ + metric_class = SUBMETRIC_TO_METRIC_CLASS_NAME[objective_metric] + metrics_config = { + "METRICS": [metric_class], + "THRESHOLD": iou_threshold, + "PRINT_CONFIG": False, + } + metrics_list = [ + getattr(trackeval.metrics, metric_name)(metrics_config) + for metric_name in cast(List[str], metrics_config["METRICS"]) + ] + dataset_config = { + **TrackEvalDataset.get_default_dataset_config(), + "GT_TRACKS": {"tracker": labels}, + "PREDICTED_TRACKS": {"tracker": track_predictions}, + "SEQ_IDS_TO_EVAL": list(labels.keys()), + "CLASSES_TO_EVAL": classes, + "TRACKERS_TO_EVAL": ["tracker"], + "OUTPUT_FOLDER": "tmp", + } + evaluator = trackeval.Evaluator( + { + **trackeval.Evaluator.get_default_eval_config(), + "PRINT_RESULTS": False, + "PRINT_CONFIG": False, + "TIME_PROGRESS": False, + "OUTPUT_SUMMARY": False, + "OUTPUT_DETAILED": False, + "PLOT_CURVES": False, + } + ) + + score_thresholds_by_class = {} + sim_func = partial(_xy_center_similarity, zero_distance=match_distance_m) + for name in classes: + single_cls_labels = _filter_by_class(labels, name) + single_cls_predictions = _filter_by_class(track_predictions, name) + score_thresholds_by_class[name] = _calculate_score_thresholds( + single_cls_labels, + single_cls_predictions, + sim_func, + num_thresholds=num_thresholds, + ) + + metric_results = [] + for threshold_i in tqdm( + range(num_thresholds), "calculating optimal track score thresholds" + ): + score_threshold_by_class = { + n: score_thresholds_by_class[n][threshold_i] for n in classes + } + filtered_predictions = utils.filter_by_class_thresholds( + track_predictions, score_threshold_by_class + ) + with contextlib.redirect_stdout( + None + ): # silence print statements from TrackEval + result_for_threshold, _ = evaluator.evaluate( + [ + TrackEvalDataset( + { + **dataset_config, + "PREDICTED_TRACKS": {"tracker": filtered_predictions}, + } + ) + ], + metrics_list, + ) + metric_results.append( + result_for_threshold["TrackEvalDataset"]["tracker"]["COMBINED_SEQ"] + ) + + optimal_score_threshold_by_class = {} + optimal_metric_values_by_class = {} + mean_metric_values_by_class = {} + for name in classes: + metric_values = [ + r[name][metric_class][objective_metric] for r in metric_results + ] + metric_values = [ + np.mean(v) if isinstance(v, np.ndarray) else v for v in metric_values + ] + optimal_threshold = score_thresholds_by_class[name][np.argmax(metric_values)] + optimal_score_threshold_by_class[name] = optimal_threshold + optimal_metric_values_by_class[name] = max(0, np.max(metric_values)) + mean_metric_values_by_class[name] = np.nanmean( + np.array(metric_values).clip(min=0) + ) + return ( + optimal_score_threshold_by_class, + optimal_metric_values_by_class, + mean_metric_values_by_class, + ) + + +def _filter_by_class(detections: Any, name: str) -> Any: + return utils.group_frames( + [ + utils.index_array_values(f, f["name"] == name) + for f in utils.ungroup_frames(detections) + ] + ) + + +def _calculate_score_thresholds( + labels: Sequences, + predictions: Sequences, + sim_func: Callable[[NDArrayFloat, NDArrayFloat], NDArrayFloat], + num_thresholds: int = 40, + min_recall: float = 0.1, +) -> NDArrayFloat: + scores, n_gt = _calculate_matched_scores(labels, predictions, sim_func) + recall_thresholds = np.linspace(min_recall, 1, num_thresholds).round(12)[::-1] + if len(scores) == 0: + return np.zeros_like(recall_thresholds) + score_thresholds = _recall_to_scores( + scores, recall_threshold=recall_thresholds, n_gt=n_gt + ) + score_thresholds = np.nan_to_num(score_thresholds, nan=0) + return score_thresholds + + +def _calculate_matched_scores( + labels: Sequences, + predictions: Sequences, + sim_func: Callable[[NDArrayFloat, NDArrayFloat], NDArrayFloat], +) -> Tuple[NDArrayFloat, int]: + scores = [] + n_gt = 0 + num_tp = 0 + for seq_id in labels: + for label_frame, prediction_frame in zip(labels[seq_id], predictions[seq_id]): + sim = sim_func( + label_frame["translation_m"], prediction_frame["translation_m"] + ) + match_rows, match_cols = linear_sum_assignment(-sim) + scores.append(prediction_frame["score"][match_cols]) + n_gt += len(label_frame["translation_m"]) + num_tp += len(match_cols) + + scores_array = np.concatenate(scores) + return scores_array, n_gt + + +def _recall_to_scores( + scores: NDArrayFloat, recall_threshold: NDArrayFloat, n_gt: int +) -> NDArrayFloat: + # Sort scores. + scores.sort() + scores = scores[::-1] + + # Determine thresholds. + recall_values = np.arange(1, len(scores) + 1) / n_gt + max_recall_achieved = np.max(recall_values) + assert max_recall_achieved <= 1 + score_thresholds = np.interp(recall_threshold, recall_values, scores, right=0) + + # Set thresholds for unachieved recall values to nan to penalize AMOTA/AMOTP later. + if isinstance(recall_threshold, np.ndarray): + score_thresholds[recall_threshold > max_recall_achieved] = np.nan + return score_thresholds + + +def _xy_center_similarity( + centers1: NDArrayFloat, centers2: NDArrayFloat, zero_distance: float +) -> NDArrayFloat: + if centers1.size == 0 or centers2.size == 0: + return np.zeros((len(centers1), len(centers2))) + xy_dist = np.linalg.norm( + centers1[:, np.newaxis, :2] - centers2[np.newaxis, :, :2], axis=2 + ) + sim = np.maximum(0, 1 - xy_dist / zero_distance) + return cast(NDArrayFloat, sim) + + +def filter_max_dist(tracks: Any, max_range_m: int) -> Any: + """Remove all tracks that are beyond the max_dist. + + Args: + tracks: Dict[seq_id: List[frame]] Dictionary of tracks + max_range_m: maximum distance from ego-vehicle + + Returns: + tracks: Dict[seq_id: List[frame]] Dictionary of tracks. + """ + frames = utils.ungroup_frames(tracks) + return utils.group_frames( + [ + utils.index_array_values( + frame, + np.linalg.norm( + frame["translation_m"][:, :2] + - np.array(frame["ego_translation_m"])[:2], + axis=1, + ) + <= max_range_m, + ) + for frame in frames + ] + ) + + +def yaw_to_quaternion3d(yaw: float) -> NDArrayFloat: + """Convert a rotation angle in the xy plane (i.e. about the z axis) to a quaternion. + + Args: + yaw: angle to rotate about the z-axis, representing an Euler angle, in radians + + Returns: + array w/ quaternion coefficients (qw,qx,qy,qz) in scalar-first order, per Argoverse convention. + """ + qx, qy, qz, qw = Rotation.from_euler(seq="z", angles=yaw, degrees=False).as_quat() + return np.array([qw, qx, qy, qz]) + + +def filter_drivable_area(tracks: Sequences, dataset_dir: Optional[str]) -> Sequences: + """Convert the unified label format to a format that is easier to work with for forecasting evaluation. + + Args: + tracks: Dictionary of tracks + dataset_dir: Dataset root directory + + Returns: + tracks: Dictionary of tracks. + """ + if dataset_dir is None: + return tracks + + log_ids = list(tracks.keys()) + log_id_to_avm, log_id_to_timestamped_poses = load_mapped_avm_and_egoposes( + log_ids, Path(dataset_dir) + ) + + for log_id in log_ids: + avm = log_id_to_avm[log_id] + + for frame in tracks[log_id]: + timestamp_ns = frame["timestamp_ns"] + city_SE3_ego = log_id_to_timestamped_poses[log_id][int(timestamp_ns)] + translation_m = frame["translation_m"] - frame["ego_translation_m"] + size = frame["size"] + quat = np.array([yaw_to_quaternion3d(yaw) for yaw in frame["yaw"]]) + score = np.ones((translation_m.shape[0], 1)) + boxes = np.concatenate([translation_m, size, quat, score], axis=1) + + is_evaluated = compute_objects_in_roi_mask(boxes, city_SE3_ego, avm) + + frame["translation_m"] = frame["translation_m"][is_evaluated] + frame["size"] = frame["size"][is_evaluated] + frame["yaw"] = frame["yaw"][is_evaluated] + frame["velocity_m_per_s"] = frame["velocity_m_per_s"][is_evaluated] + frame["label"] = frame["label"][is_evaluated] + frame["name"] = frame["name"][is_evaluated] + frame["track_id"] = frame["track_id"][is_evaluated] + + if "score" in frame: + frame["score"] = frame["score"][is_evaluated] + + if "detection_score" in frame: + frame["detection_score"] = frame["detection_score"][is_evaluated] + + if "xy" in frame: + frame["xy"] = frame["xy"][is_evaluated] + + if "xy_velocity" in frame: + frame["xy_velocity"] = frame["xy_velocity"][is_evaluated] + + if "active" in frame: + frame["active"] = frame["active"][is_evaluated] + + if "age" in frame: + frame["age"] = frame["age"][is_evaluated] + + return tracks + + +def evaluate( + track_predictions: Sequences, + labels: Sequences, + objective_metric: str, + max_range_m: int, + dataset_dir: Any, + out: str, +) -> Tuple[Dict[str, float], Dict[str, Any], Dict[str, Any]]: + """Run evaluation. + + Args: + track_predictions: Dictionary of tracks. + labels: Dictionary of labels. + objective_metric: Metric to optimize. + max_range_m: Maximum evaluation range. + dataset_dir: Path to dataset. Required for ROI pruning. + out: Output path. + + Returns: + Dictionary of per-category metrics. + """ + classes = list(constants.AV2_CATEGORIES) + + labels = filter_max_dist(labels, max_range_m) + utils.annotate_frame_metadata( + utils.ungroup_frames(track_predictions), + utils.ungroup_frames(labels), + ["ego_translation_m"], + ) + track_predictions = filter_max_dist(track_predictions, max_range_m) + + if dataset_dir is not None: + labels = filter_drivable_area(labels, dataset_dir) + track_predictions = filter_drivable_area(track_predictions, dataset_dir) + + score_thresholds, tuned_metric_values, mean_metric_values = _tune_score_thresholds( + labels, + track_predictions, + objective_metric, + classes, + num_thresholds=10, + match_distance_m=2, + ) + filtered_track_predictions = utils.filter_by_class_thresholds( + track_predictions, score_thresholds + ) + res = evaluate_tracking( + labels, + filtered_track_predictions, + classes, + tracker_name="TRACKER", + output_dir=".".join(out.split("/")[:-1]), + ) + + return res, tuned_metric_values, mean_metric_values + + +@click.command() +@click.option("--predictions", required=True, help="Predictions PKL file") +@click.option("--ground_truth", required=True, help="Ground Truth PKL file") +@click.option("--max_range_m", default=50, type=int, help="Predictions PKL file") +@click.option( + "--dataset_dir", + default=None, + help="Path to dataset split (e.g. /data/Sensor/val). Required for ROI pruning", +) +@click.option("--objective_metric", default="HOTA", help="Choices: HOTA, MOTA") +@click.option("--out", required=True, help="Output JSON file") +def runner( + predictions: str, + ground_truth: str, + max_range_m: int, + dataset_dir: Any, + objective_metric: str, + out: str, +) -> None: + """Standalone evaluation function.""" + track_predictions = pickle.load(open(predictions, "rb")) + labels = pickle.load(open(ground_truth, "rb")) + + _, _, mean_metric_values = evaluate( + track_predictions, labels, objective_metric, max_range_m, dataset_dir, out + ) + + pprint(mean_metric_values) + + with open(out, "w") as f: + json.dump(mean_metric_values, f, indent=4) + + +if __name__ == "__main__": + runner() diff --git a/src/av2/evaluation/tracking/utils.py b/src/av2/evaluation/tracking/utils.py new file mode 100644 index 00000000..49605583 --- /dev/null +++ b/src/av2/evaluation/tracking/utils.py @@ -0,0 +1,177 @@ +"""Tracking evaluation utilities. + +Detection and track data in a single frame are kept as a dictionary of names to numpy arrays. +This module provides helper functions for manipulating this data format. +""" + +import os +import pickle +from collections import defaultdict +from itertools import chain +from typing import Any, Dict, Iterable, List, Union, cast + +import numpy as np + +from av2.utils.typing import NDArrayInt + +from ..typing import Frame, Frames, Sequences + + +def save(obj: Any, path: str) -> None: # noqa + """Save an object to a file using pickle serialization. + + Args: + obj: An object to be saved. + path: A string representing the file path to save the object to. + """ + dir = os.path.dirname(path) + if dir != "": + os.makedirs(dir, exist_ok=True) + with open(path, "wb") as f: + pickle.dump(obj, f) + + +def load(path: str) -> Any: # noqa + """Load an object from file using pickle module. + + Args: + path: File path. + + Returns: + Object or None if the file does not exist. + """ + if not os.path.exists(path): + return None + with open(path, "rb") as f: + return pickle.load(f) + + +def annotate_frame_metadata( + prediction_frames: Frames, label_frames: Frames, metadata_keys: List[str] +) -> None: + """Copy annotations with provided keys from label to prediction frames. + + Args: + prediction_frames: list of prediction frames + label_frames: list of label frames + metadata_keys: keys of the annotations to be copied. + """ + assert len(prediction_frames) == len(label_frames) + for prediction, label in zip(prediction_frames, label_frames): + for key in metadata_keys: + prediction[key] = label[key] + + +def group_frames(frames_list: Frames) -> Sequences: + """Group list of frames into dictionary by sequence id. + + Args: + frames_list: List of frames, each containing a detections snapshot at timestamp_ns. + + Returns: + Dictionary of frames indexed by sequence id. + """ + frames_by_seq_id = defaultdict(list) + sorted_frames_list = sorted(frames_list, key=lambda f: cast(int, f["timestamp_ns"])) + for frame in sorted_frames_list: + frames_by_seq_id[frame["seq_id"]].append(frame) + return dict(frames_by_seq_id) + + +def ungroup_frames(frames_by_seq_id: Sequences) -> Frames: + """Ungroup dictionary of frames into a list of frames. + + Args: + frames_by_seq_id: dictionary of frames + + Returns: + List of frames + """ + return list(chain.from_iterable(frames_by_seq_id.values())) + + +def index_array_values(array_dict: Frame, index: Union[int, NDArrayInt]) -> Frame: + """Index each numpy array in dictionary. + + Args: + array_dict: dictionary of numpy arrays + index: index used to access each numpy array in array_dict + + Returns: + Dictionary of numpy arrays, each indexed by the provided index + """ + return { + k: v[index] if isinstance(v, np.ndarray) else v for k, v in array_dict.items() + } + + +def array_dict_iterator(array_dict: Frame, length: int) -> Iterable[Frame]: + """Get an iterator over each index in array_dict. + + Args: + array_dict: dictionary of numpy arrays + length: number of elements to iterate over + + Returns: + Iterator, each element is a dictionary of numpy arrays, indexed from 0 to (length-1) + """ + return (index_array_values(array_dict, i) for i in range(length)) + + +def concatenate_array_values(array_dicts: Frames) -> Frame: + """Concatenates numpy arrays in list of dictionaries. + + Handles inconsistent keys (will skip missing keys) + Does not concatenate non-numpy values (int, str), sets to value if all values are equal + + Args: + array_dicts: list of dictionaries + + Returns: + single dictionary of names to numpy arrays + """ + combined = defaultdict(list) + for array_dict in array_dicts: + for k, v in array_dict.items(): + combined[k].append(v) + concatenated = {} + for k, vs in combined.items(): + if all(isinstance(v, np.ndarray) for v in vs): + if any(v.size > 0 for v in vs): + concatenated[k] = np.concatenate([v for v in vs if v.size > 0]) + else: + concatenated[k] = vs[0] + elif all(vs[0] == v for v in vs): + concatenated[k] = vs[0] + return concatenated + + +def filter_by_class_thresholds( + frames_by_seq_id: Sequences, thresholds_by_class: Dict[str, float] +) -> Sequences: + """Filter detections, keeping only detections with score higher than the provided threshold for that class. + + If a class threshold is not provided, all detections in that class is filtered. + + Args: + frames_by_seq_id: Dictionary of frames + thresholds_by_class: Dictionary containing the score thresholds for each class + + Returns: + Dictionary of frames, filtered by class score thresholds + """ + frames = ungroup_frames(frames_by_seq_id) + return group_frames( + [ + concatenate_array_values( + [ + index_array_values( + frame, + (frame["name"] == class_name) & (frame["score"] >= threshold), + ) + for class_name, threshold in thresholds_by_class.items() + ] + ) + for frame in frames + ] + ) diff --git a/src/av2/evaluation/typing.py b/src/av2/evaluation/typing.py new file mode 100644 index 00000000..9dfa574c --- /dev/null +++ b/src/av2/evaluation/typing.py @@ -0,0 +1,8 @@ +"""Types for evaluation.""" + +from typing import Any, Dict, List + +Frame = Dict[str, Any] +Frames = List[Frame] +Sequences = Dict[str, Frames] +ForecastSequences = Dict[str, Dict[int, List[Frame]]] diff --git a/src/av2/geometry/camera/pinhole_camera.py b/src/av2/geometry/camera/pinhole_camera.py index 4e52d928..dd18f058 100644 --- a/src/av2/geometry/camera/pinhole_camera.py +++ b/src/av2/geometry/camera/pinhole_camera.py @@ -108,7 +108,9 @@ def from_feather(cls, log_dir: Path, cam_name: str) -> PinholeCamera: cam_name=cam_name, ) - def cull_to_view_frustum(self, uv: NDArrayFloat, points_cam: NDArrayFloat) -> NDArrayBool: + def cull_to_view_frustum( + self, uv: NDArrayFloat, points_cam: NDArrayFloat + ) -> NDArrayBool: """Cull 3d points to camera view frustum. Given a set of coordinates in the image plane and corresponding points @@ -131,7 +133,9 @@ def cull_to_view_frustum(self, uv: NDArrayFloat, points_cam: NDArrayFloat) -> ND is_valid_x = np.logical_and(0 <= uv[:, 0], uv[:, 0] < self.width_px - 1) is_valid_y = np.logical_and(0 <= uv[:, 1], uv[:, 1] < self.height_px - 1) is_valid_z = points_cam[:, 2] > 0 - is_valid_points: NDArrayBool = np.logical_and.reduce([is_valid_x, is_valid_y, is_valid_z]) + is_valid_points: NDArrayBool = np.logical_and.reduce( + [is_valid_x, is_valid_y, is_valid_z] + ) return is_valid_points def project_ego_to_img( @@ -249,8 +253,12 @@ def project_ego_to_img_motion_compensated( if city_SE3_ego_lidar_t is None: raise ValueError("city_SE3_ego_lidar_t cannot be `None`!") - ego_cam_t_SE3_ego_lidar_t = city_SE3_ego_cam_t.inverse().compose(city_SE3_ego_lidar_t) - points_cam_time = ego_cam_t_SE3_ego_lidar_t.transform_point_cloud(points_lidar_time) + ego_cam_t_SE3_ego_lidar_t = city_SE3_ego_cam_t.inverse().compose( + city_SE3_ego_lidar_t + ) + points_cam_time = ego_cam_t_SE3_ego_lidar_t.transform_point_cloud( + points_lidar_time + ) return self.project_ego_to_img(points_cam_time) @cached_property @@ -261,7 +269,7 @@ def right_clipping_plane(self) -> NDArrayFloat: (4,) tuple of Hessian normal coefficients. """ a, b, c, d = -self.intrinsics.fx_px, 0.0, self.width_px / 2.0, 0.0 - coeffs: NDArrayFloat = np.array([a, b, c, d]) / np.linalg.norm([a, b, c]) # type: ignore + coeffs: NDArrayFloat = np.array([a, b, c, d]) / np.linalg.norm([a, b, c]) return coeffs @cached_property @@ -272,7 +280,7 @@ def left_clipping_plane(self) -> NDArrayFloat: (4,) tuple of Hessian normal coefficients. """ a, b, c, d = self.intrinsics.fx_px, 0.0, self.width_px / 2.0, 0.0 - coeffs: NDArrayFloat = np.array([a, b, c, d]) / np.linalg.norm([a, b, c]) # type: ignore + coeffs: NDArrayFloat = np.array([a, b, c, d]) / np.linalg.norm([a, b, c]) return coeffs @cached_property @@ -283,7 +291,7 @@ def top_clipping_plane(self) -> NDArrayFloat: (4,) tuple of Hessian normal coefficients. """ a, b, c, d = 0.0, self.intrinsics.fx_px, self.height_px / 2.0, 0.0 - coeffs: NDArrayFloat = np.array([a, b, c, d]) / np.linalg.norm([a, b, c]) # type: ignore + coeffs: NDArrayFloat = np.array([a, b, c, d]) / np.linalg.norm([a, b, c]) return coeffs @cached_property @@ -294,7 +302,7 @@ def bottom_clipping_plane(self) -> NDArrayFloat: (4,) tuple of Hessian normal coefficients. """ a, b, c, d = 0.0, -self.intrinsics.fx_px, self.height_px / 2.0, 0.0 - coeffs: NDArrayFloat = np.array([a, b, c, d]) / np.linalg.norm([a, b, c]) # type: ignore + coeffs: NDArrayFloat = np.array([a, b, c, d]) / np.linalg.norm([a, b, c]) return coeffs def near_clipping_plane(self, near_clip_m: float) -> NDArrayFloat: @@ -323,7 +331,7 @@ def frustum_planes(self, near_clip_dist: float = 0.5) -> NDArrayFloat: near_clip_dist: Distance of the near clipping plane from the origin. Returns: - (5, 4) matrix where each row corresponds to the coeffients of a plane. + (5, 4) matrix where each row corresponds to the coefficients of a plane. """ left_plane = self.left_clipping_plane right_plane = self.right_clipping_plane @@ -332,7 +340,9 @@ def frustum_planes(self, near_clip_dist: float = 0.5) -> NDArrayFloat: bottom_plane = self.bottom_clipping_plane near_plane = self.near_clipping_plane(near_clip_dist) - planes: NDArrayFloat = np.stack([left_plane, right_plane, near_plane, bottom_plane, top_plane]) + planes: NDArrayFloat = np.stack( + [left_plane, right_plane, near_plane, bottom_plane, top_plane] + ) return planes @cached_property @@ -363,7 +373,9 @@ def fov_theta_rad(self) -> float: fov_theta_rad = 2 * np.arctan(0.5 * self.width_px / self.intrinsics.fx_px) return float(fov_theta_rad) - def compute_pixel_ray_directions(self, uv: Union[NDArrayFloat, NDArrayInt]) -> NDArrayFloat: + def compute_pixel_ray_directions( + self, uv: Union[NDArrayFloat, NDArrayInt] + ) -> NDArrayFloat: """Given (u,v) coordinates and intrinsics, generate pixel rays in the camera coordinate frame. Assume +z points out of the camera, +y is downwards, and +x is across the imager. @@ -382,7 +394,9 @@ def compute_pixel_ray_directions(self, uv: Union[NDArrayFloat, NDArrayInt]) -> N img_h, img_w = self.height_px, self.width_px if not np.isclose(fx, fy, atol=1e-3): - raise ValueError(f"Focal lengths in the x and y directions must match: {fx} != {fy}") + raise ValueError( + f"Focal lengths in the x and y directions must match: {fx} != {fy}" + ) if uv.shape[1] != 2: raise ValueError("Input (u,v) coordinates must be (N,2) in shape.") @@ -402,7 +416,7 @@ def compute_pixel_ray_directions(self, uv: Union[NDArrayFloat, NDArrayInt]) -> N ray_dirs[:, 2] = fx # elementwise multiplication of scalars requires last dim to match - ray_dirs = ray_dirs / np.linalg.norm(ray_dirs, axis=1, keepdims=True) # type: ignore + ray_dirs = ray_dirs / np.linalg.norm(ray_dirs, axis=1, keepdims=True) if ray_dirs.shape[1] != 3: raise RuntimeError("Ray directions must be (N,3)") return ray_dirs @@ -424,10 +438,14 @@ def scale(self, scale: float) -> PinholeCamera: round(self.intrinsics.width_px * scale), round(self.intrinsics.height_px * scale), ) - return PinholeCamera(ego_SE3_cam=self.ego_SE3_cam, intrinsics=intrinsics, cam_name=self.cam_name) + return PinholeCamera( + ego_SE3_cam=self.ego_SE3_cam, intrinsics=intrinsics, cam_name=self.cam_name + ) -def remove_nan_values(uv: NDArrayFloat, points_cam: NDArrayFloat) -> Tuple[NDArrayFloat, NDArrayFloat]: +def remove_nan_values( + uv: NDArrayFloat, points_cam: NDArrayFloat +) -> Tuple[NDArrayFloat, NDArrayFloat]: """Remove NaN values from camera coordinates and image plane coordinates (accepts corrupt array). Args: diff --git a/src/av2/geometry/geometry.py b/src/av2/geometry/geometry.py index 8865765a..c735a249 100644 --- a/src/av2/geometry/geometry.py +++ b/src/av2/geometry/geometry.py @@ -170,7 +170,7 @@ def cart_to_sph(xyz: NDArrayFloat) -> NDArrayFloat: def cart_to_hom(cart: NDArrayFloat) -> NDArrayFloat: - """Convert Cartesian coordinates into Homogenous coordinates. + """Convert Cartesian coordinates into Homogeneous coordinates. This function converts a set of points in R^N to its homogeneous representation in R^(N+1). @@ -187,7 +187,7 @@ def cart_to_hom(cart: NDArrayFloat) -> NDArrayFloat: def hom_to_cart(hom: NDArrayFloat) -> NDArrayFloat: - """Convert Homogenous coordinates into Cartesian coordinates. + """Convert Homogeneous coordinates into Cartesian coordinates. This function converts a set of points in R^(N+1) to its Cartesian representation in R^N. @@ -229,11 +229,15 @@ def crop_points( # Ensure that the logical operations will broadcast. if n_dim != lb_dim or n_dim != ub_dim: - raise ValueError(f"Dimensions n_dim {n_dim} must match both lb_dim {lb_dim} and ub_dim {ub_dim}") + raise ValueError( + f"Dimensions n_dim {n_dim} must match both lb_dim {lb_dim} and ub_dim {ub_dim}" + ) # Ensure that the lower bound less than or equal to the upper bound for each dimension. if not all(lb < ub for lb, ub in zip(lower_bound_inclusive, upper_bound_exclusive)): - raise ValueError("Lower bound must be less than or equal to upper bound for each dimension") + raise ValueError( + "Lower bound must be less than or equal to upper bound for each dimension" + ) # Lower bound mask. lb_mask = np.greater_equal(points, lower_bound_inclusive) @@ -246,7 +250,9 @@ def crop_points( return points[is_valid_points], is_valid_points -def compute_interior_points_mask(points_xyz: NDArrayFloat, cuboid_vertices: NDArrayFloat) -> NDArrayBool: +def compute_interior_points_mask( + points_xyz: NDArrayFloat, cuboid_vertices: NDArrayFloat +) -> NDArrayBool: r"""Compute the interior points mask for the cuboid. Reference: https://math.stackexchange.com/questions/1472049/check-if-a-point-is-inside-a-rectangular-shaped-area-3d @@ -266,12 +272,14 @@ def compute_interior_points_mask(points_xyz: NDArrayFloat, cuboid_vertices: NDAr Args: points_xyz: (N,3) Array representing a point cloud in Cartesian coordinates (x,y,z). cuboid_vertices: (8,3) Array representing 3D cuboid vertices, ordered as shown above. - + Returns: (N,) An array of boolean flags indicating whether the points are interior to the cuboid. """ # Get three corners of the cuboid vertices. - vertices: NDArrayFloat = np.stack((cuboid_vertices[6], cuboid_vertices[3], cuboid_vertices[1])) # (3,3) + vertices: NDArrayFloat = np.stack( + (cuboid_vertices[6], cuboid_vertices[3], cuboid_vertices[1]) + ) # (3,3) # Choose reference vertex. # vertices and choice of ref_vertex are coupled. @@ -285,11 +293,15 @@ def compute_interior_points_mask(points_xyz: NDArrayFloat, cuboid_vertices: NDAr sim_uvw_ref = uvw @ ref_vertex # (3,) # Only care about the diagonal. - sim_uvw_vertices: NDArrayFloat = np.diag(uvw @ vertices.transpose()) # type: ignore # (3,) + sim_uvw_vertices: NDArrayFloat = np.diag(uvw @ vertices.transpose()) # (3,) # Check 6 conditions (2 for each of the 3 orthogonal directions). # Refer to the linked reference for additional information. - constraint_a = np.logical_and(sim_uvw_ref <= sim_uvw_points, sim_uvw_points <= sim_uvw_vertices) - constraint_b = np.logical_and(sim_uvw_ref >= sim_uvw_points, sim_uvw_points >= sim_uvw_vertices) + constraint_a = np.logical_and( + sim_uvw_ref <= sim_uvw_points, sim_uvw_points <= sim_uvw_vertices + ) + constraint_b = np.logical_and( + sim_uvw_ref >= sim_uvw_points, sim_uvw_points >= sim_uvw_vertices + ) is_interior: NDArrayBool = np.logical_or(constraint_a, constraint_b).all(axis=1) return is_interior diff --git a/src/av2/geometry/infinity_norm_utils.py b/src/av2/geometry/infinity_norm_utils.py index 959a65a4..2e477880 100644 --- a/src/av2/geometry/infinity_norm_utils.py +++ b/src/av2/geometry/infinity_norm_utils.py @@ -7,7 +7,9 @@ from av2.utils.typing import NDArrayFloat -def has_pts_in_infinity_norm_radius(points: NDArrayFloat, window_center: NDArrayFloat, window_sz: float) -> bool: +def has_pts_in_infinity_norm_radius( + points: NDArrayFloat, window_center: NDArrayFloat, window_sz: float +) -> bool: """Check if a map entity has points within a search radius from a single query point. Note: Distance is measured by the infinity norm. @@ -24,7 +26,9 @@ def has_pts_in_infinity_norm_radius(points: NDArrayFloat, window_center: NDArray ValueError: If `points` is not in R^2 or R^3. """ if points.ndim != 2 or points.shape[1] not in (2, 3): - raise ValueError(f"Input points array must have shape (N,2) or (N,3) - received {points.shape}.") + raise ValueError( + f"Input points array must have shape (N,2) or (N,3) - received {points.shape}." + ) if points.shape[1] == 3: # take only x,y dimensions @@ -38,5 +42,5 @@ def has_pts_in_infinity_norm_radius(points: NDArrayFloat, window_center: NDArray # reshape just in case was given column vector window_center = window_center.reshape(1, 2) - dists = np.linalg.norm(points - window_center, ord=np.inf, axis=1) # type: ignore + dists = np.linalg.norm(points - window_center, ord=np.inf, axis=1) return bool(dists.min() < window_sz) diff --git a/src/av2/geometry/interpolate.py b/src/av2/geometry/interpolate.py index 8b0af1bb..ec929d73 100644 --- a/src/av2/geometry/interpolate.py +++ b/src/av2/geometry/interpolate.py @@ -14,7 +14,9 @@ NUM_CENTERLINE_INTERP_PTS: Final[int] = 10 -def compute_lane_width(left_even_pts: NDArrayFloat, right_even_pts: NDArrayFloat) -> float: +def compute_lane_width( + left_even_pts: NDArrayFloat, right_even_pts: NDArrayFloat +) -> float: """Compute the width of a lane, given an explicit left and right boundary. Requires an equal number of waypoints on each boundary. For 3d polylines, this incorporates @@ -35,11 +37,13 @@ def compute_lane_width(left_even_pts: NDArrayFloat, right_even_pts: NDArrayFloat raise ValueError( f"Shape of left_even_pts {left_even_pts.shape} did not match right_even_pts {right_even_pts.shape}" ) - lane_width = float(np.mean(np.linalg.norm(left_even_pts - right_even_pts, axis=1))) # type: ignore + lane_width = float(np.mean(np.linalg.norm(left_even_pts - right_even_pts, axis=1))) return lane_width -def compute_mid_pivot_arc(single_pt: NDArrayFloat, arc_pts: NDArrayFloat) -> Tuple[NDArrayFloat, float]: +def compute_mid_pivot_arc( + single_pt: NDArrayFloat, arc_pts: NDArrayFloat +) -> Tuple[NDArrayFloat, float]: """Compute an arc by pivoting around a single point. Given a line of points on one boundary, and a single point on the other side, @@ -57,7 +61,7 @@ def compute_mid_pivot_arc(single_pt: NDArrayFloat, arc_pts: NDArrayFloat) -> Tup """ num_pts = len(arc_pts) # form ladder with equal number of vertices on each side - single_pt_tiled = np.tile(single_pt, (num_pts, 1)) # type: ignore + single_pt_tiled = np.tile(single_pt, (num_pts, 1)) # compute midpoint for each rung of the ladder centerline_pts = (single_pt_tiled + arc_pts) / 2.0 lane_width = compute_lane_width(single_pt_tiled, arc_pts) @@ -90,7 +94,9 @@ def compute_midpoint_line( ValueError: If the left and right lane boundaries aren't a list of 2d or 3d waypoints. """ if left_ln_boundary.ndim != 2 or right_ln_boundary.ndim != 2: - raise ValueError("Left and right lane boundaries must consist of a sequence of 2d or 3d waypoints.") + raise ValueError( + "Left and right lane boundaries must consist of a sequence of 2d or 3d waypoints." + ) dim = left_ln_boundary.shape[1] if dim not in [2, 3]: @@ -100,18 +106,22 @@ def compute_midpoint_line( raise ValueError("Left ") if len(left_ln_boundary) == 1: - centerline_pts, lane_width = compute_mid_pivot_arc(single_pt=left_ln_boundary, arc_pts=right_ln_boundary) + centerline_pts, lane_width = compute_mid_pivot_arc( + single_pt=left_ln_boundary, arc_pts=right_ln_boundary + ) return centerline_pts[:, :2], lane_width if len(right_ln_boundary) == 1: - centerline_pts, lane_width = compute_mid_pivot_arc(single_pt=right_ln_boundary, arc_pts=left_ln_boundary) + centerline_pts, lane_width = compute_mid_pivot_arc( + single_pt=right_ln_boundary, arc_pts=left_ln_boundary + ) return centerline_pts[:, :2], lane_width # fall back to the typical case. left_even_pts = interp_arc(num_interp_pts, points=left_ln_boundary) right_even_pts = interp_arc(num_interp_pts, points=right_ln_boundary) - centerline_pts = (left_even_pts + right_even_pts) / 2.0 # type: ignore + centerline_pts = (left_even_pts + right_even_pts) / 2.0 lane_width = compute_lane_width(left_even_pts, right_even_pts) return centerline_pts, lane_width @@ -151,7 +161,7 @@ def interp_arc(t: int, points: NDArrayFloat) -> NDArrayFloat: # Compute the chordal arclength of each segment. # Compute differences between each x coord, to get the dx's # Do the same to get dy's. Then the hypotenuse length is computed as a norm. - chordlen: NDArrayFloat = np.linalg.norm(np.diff(points, axis=0), axis=1) # type: ignore + chordlen: NDArrayFloat = np.linalg.norm(np.diff(points, axis=0), axis=1) # Normalize the arclengths to a unit total chordlen = chordlen / np.sum(chordlen) # cumulative arclength @@ -160,10 +170,10 @@ def interp_arc(t: int, points: NDArrayFloat) -> NDArrayFloat: cumarc[1:] = np.cumsum(chordlen) # which interval did each point fall in, in terms of eq_spaced_points? (bin index) - tbins: NDArrayInt = np.digitize(eq_spaced_points, bins=cumarc).astype(int) # type: ignore + tbins: NDArrayInt = np.digitize(eq_spaced_points, bins=cumarc).astype(int) # #catch any problems at the ends - tbins[np.where((tbins <= 0) | (eq_spaced_points <= 0))] = 1 # type: ignore + tbins[np.where((tbins <= 0) | (eq_spaced_points <= 0))] = 1 tbins[np.where((tbins >= n) | (eq_spaced_points >= 1))] = n - 1 s = np.divide((eq_spaced_points - cumarc[tbins - 1]), chordlen[tbins - 1]) @@ -176,7 +186,9 @@ def interp_arc(t: int, points: NDArrayFloat) -> NDArrayFloat: def linear_interpolation( - key_timestamps: Tuple[int, int], key_translations: Tuple[NDArrayFloat, NDArrayFloat], query_timestamp: int + key_timestamps: Tuple[int, int], + key_translations: Tuple[NDArrayFloat, NDArrayFloat], + query_timestamp: int, ) -> NDArrayFloat: """Given two 3d positions at specific timestamps, interpolate an intermediate position at a given timestamp. @@ -193,17 +205,19 @@ def linear_interpolation( """ t0, t1 = key_timestamps if query_timestamp < t0 or query_timestamp > t1: - raise ValueError("Query timestamp must be witin the interval [t0,t1].") + raise ValueError("Query timestamp must be within the interval [t0,t1].") interval = t1 - t0 t = (query_timestamp - t0) / interval - vec = key_translations[1] - key_translations[0] # type: ignore - translation_interp = key_translations[0] + vec * t # type: ignore + vec = key_translations[1] - key_translations[0] + translation_interp = key_translations[0] + vec * t return translation_interp -def interpolate_pose(key_timestamps: Tuple[int, int], key_poses: Tuple[SE3, SE3], query_timestamp: int) -> SE3: +def interpolate_pose( + key_timestamps: Tuple[int, int], key_poses: Tuple[SE3, SE3], query_timestamp: int +) -> SE3: """Given two SE(3) poses at specific timestamps, interpolate an intermediate pose at a given timestamp. Note: we use a straight line interpolation for the translation, while still using interpolate (aka "slerp") @@ -226,7 +240,7 @@ def interpolate_pose(key_timestamps: Tuple[int, int], key_poses: Tuple[SE3, SE3] """ t0, t1 = key_timestamps if query_timestamp < t0 or query_timestamp > t1: - raise ValueError("Query timestamp must be witin the interval [t0,t1].") + raise ValueError("Query timestamp must be within the interval [t0,t1].") # Setup the fixed keyframe rotations and times key_rots = Rotation.from_matrix(np.array([kp.rotation for kp in key_poses])) @@ -236,6 +250,10 @@ def interpolate_pose(key_timestamps: Tuple[int, int], key_poses: Tuple[SE3, SE3] R_interp = slerp(query_timestamp).as_matrix() key_translations = (key_poses[0].translation, key_poses[1].translation) - t_interp = linear_interpolation(key_timestamps, key_translations=key_translations, query_timestamp=query_timestamp) + t_interp = linear_interpolation( + key_timestamps, + key_translations=key_translations, + query_timestamp=query_timestamp, + ) pose_interp = SE3(rotation=R_interp, translation=t_interp) return pose_interp diff --git a/src/av2/geometry/iou.py b/src/av2/geometry/iou.py index ca20a526..aa5b0d92 100644 --- a/src/av2/geometry/iou.py +++ b/src/av2/geometry/iou.py @@ -7,7 +7,9 @@ from av2.utils.typing import NDArrayFloat -def iou_3d_axis_aligned(src_dims_m: NDArrayFloat, target_dims_m: NDArrayFloat) -> NDArrayFloat: +def iou_3d_axis_aligned( + src_dims_m: NDArrayFloat, target_dims_m: NDArrayFloat +) -> NDArrayFloat: """Compute 3d, axis-aligned (vertical axis alignment) intersection-over-union (IoU) between two sets of cuboids. Both objects are aligned to their +x axis and their centroids are placed at the origin diff --git a/src/av2/geometry/mesh_grid.py b/src/av2/geometry/mesh_grid.py index 275d3a80..8fb24621 100644 --- a/src/av2/geometry/mesh_grid.py +++ b/src/av2/geometry/mesh_grid.py @@ -33,7 +33,7 @@ def get_mesh_grid_as_point_cloud( ny = max_y - min_y x = np.linspace(min_x, max_x, math.ceil(nx / downsample_factor) + 1) y = np.linspace(min_y, max_y, math.ceil(ny / downsample_factor) + 1) - x_grid, y_grid = np.meshgrid(x, y) # type: ignore + x_grid, y_grid = np.meshgrid(x, y) x_grid = x_grid.flatten() y_grid = y_grid.flatten() diff --git a/src/av2/geometry/polyline_utils.py b/src/av2/geometry/polyline_utils.py index e3cf9b8e..fce300fe 100644 --- a/src/av2/geometry/polyline_utils.py +++ b/src/av2/geometry/polyline_utils.py @@ -30,11 +30,13 @@ def get_polyline_length(polyline: NDArrayFloat) -> float: """ if polyline.shape[1] not in [2, 3]: raise RuntimeError("Polyline must have shape (N,2) or (N,3)") - offsets = np.diff(polyline, axis=0) # type: ignore - return float(np.linalg.norm(offsets, axis=1).sum()) # type: ignore + offsets = np.diff(polyline, axis=0) + return float(np.linalg.norm(offsets, axis=1).sum()) -def interp_polyline_by_fixed_waypt_interval(polyline: NDArrayFloat, waypt_interval: float) -> Tuple[NDArrayFloat, int]: +def interp_polyline_by_fixed_waypt_interval( + polyline: NDArrayFloat, waypt_interval: float +) -> Tuple[NDArrayFloat, int]: """Resample waypoints of a polyline so that waypoints appear roughly at fixed intervals from the start. Args: @@ -61,7 +63,9 @@ def interp_polyline_by_fixed_waypt_interval(polyline: NDArrayFloat, waypt_interv return interp_polyline, num_waypts -def get_double_polylines(polyline: NDArrayFloat, width_scaling_factor: float) -> Tuple[NDArrayFloat, NDArrayFloat]: +def get_double_polylines( + polyline: NDArrayFloat, width_scaling_factor: float +) -> Tuple[NDArrayFloat, NDArrayFloat]: """Treat any polyline as a centerline, and extend a narrow strip on both sides. Dimension is preserved (2d->2d, and 3d->3d). @@ -75,20 +79,26 @@ def get_double_polylines(polyline: NDArrayFloat, width_scaling_factor: float) -> left: array of shape (N,2) or (N,3) representing left polyline. right: array of shape (N,2) or (N,3) representing right polyline. """ - double_line_polygon = centerline_to_polygon(centerline=polyline, width_scaling_factor=width_scaling_factor) + double_line_polygon = centerline_to_polygon( + centerline=polyline, width_scaling_factor=width_scaling_factor + ) num_pts = double_line_polygon.shape[0] # split index -- polygon from right boundary, left boundary, then close it w/ 0th vertex of right # we swap left and right since our polygon is generated about a boundary, not a centerline k = num_pts // 2 left = double_line_polygon[:k] - right = double_line_polygon[k:-1] # throw away the last point, since it is just a repeat + right = double_line_polygon[ + k:-1 + ] # throw away the last point, since it is just a repeat return left, right def swap_left_and_right( - condition: NDArrayBool, left_centerline: NDArrayFloat, right_centerline: NDArrayFloat + condition: NDArrayBool, + left_centerline: NDArrayFloat, + right_centerline: NDArrayFloat, ) -> Tuple[NDArrayFloat, NDArrayFloat]: """Swap points in left and right centerline according to condition. @@ -130,12 +140,12 @@ def centerline_to_polygon( Numpy array of shape (2N+1,2) or (2N+1,3), with duplicate first and last vertices. """ # eliminate duplicates - _, inds = np.unique(centerline, axis=0, return_index=True) # type: ignore + _, inds = np.unique(centerline, axis=0, return_index=True) # does not return indices in sorted order inds = np.sort(inds) centerline = centerline[inds] - grad: NDArrayFloat = np.gradient(centerline, axis=0) # type: ignore + grad: NDArrayFloat = np.gradient(centerline, axis=0) dx = grad[:, 0] dy = grad[:, 1] @@ -147,7 +157,9 @@ def centerline_to_polygon( x_disp = AVG_LANE_WIDTH_M * width_scaling_factor / 2.0 * np.cos(thetas) y_disp = AVG_LANE_WIDTH_M * width_scaling_factor / 2.0 * np.sin(thetas) - displacement: NDArrayFloat = np.hstack([x_disp[:, np.newaxis], y_disp[:, np.newaxis]]) + displacement: NDArrayFloat = np.hstack( + [x_disp[:, np.newaxis], y_disp[:, np.newaxis]] + ) # preserve z coordinates. right_centerline = centerline.copy() @@ -160,16 +172,24 @@ def centerline_to_polygon( subtract_cond1 = np.logical_and(dx > 0, dy < 0) subtract_cond2 = np.logical_and(dx > 0, dy > 0) subtract_cond = np.logical_or(subtract_cond1, subtract_cond2) - left_centerline, right_centerline = swap_left_and_right(subtract_cond, left_centerline, right_centerline) + left_centerline, right_centerline = swap_left_and_right( + subtract_cond, left_centerline, right_centerline + ) # right centerline also depended on if we added or subtracted y neg_disp_cond = displacement[:, 1] > 0 - left_centerline, right_centerline = swap_left_and_right(neg_disp_cond, left_centerline, right_centerline) + left_centerline, right_centerline = swap_left_and_right( + neg_disp_cond, left_centerline, right_centerline + ) if visualize: plt.scatter(centerline[:, 0], centerline[:, 1], 20, marker=".", color="b") - plt.scatter(right_centerline[:, 0], right_centerline[:, 1], 20, marker=".", color="r") - plt.scatter(left_centerline[:, 0], left_centerline[:, 1], 20, marker=".", color="g") + plt.scatter( + right_centerline[:, 0], right_centerline[:, 1], 20, marker=".", color="r" + ) + plt.scatter( + left_centerline[:, 0], left_centerline[:, 1], 20, marker=".", color="g" + ) fname = datetime.datetime.utcnow().strftime("%Y_%m_%d_%H_%M_%S_%f") plt.savefig(f"polygon_unit_tests/{fname}.png") plt.close("all") @@ -178,7 +198,9 @@ def centerline_to_polygon( return convert_lane_boundaries_to_polygon(right_centerline, left_centerline) -def convert_lane_boundaries_to_polygon(right_lane_bounds: NDArrayFloat, left_lane_bounds: NDArrayFloat) -> NDArrayFloat: +def convert_lane_boundaries_to_polygon( + right_lane_bounds: NDArrayFloat, left_lane_bounds: NDArrayFloat +) -> NDArrayFloat: """Convert lane boundaries to a polygon. Given left and right boundaries of a lane segment, provide the exterior vertices of the @@ -203,9 +225,13 @@ def convert_lane_boundaries_to_polygon(right_lane_bounds: NDArrayFloat, left_lan RuntimeError: If the last dimension of the left and right boundary polylines do not match. """ if not right_lane_bounds.shape[-1] == left_lane_bounds.shape[-1]: - raise RuntimeError("Last dimension of left and right boundary polylines must match.") + raise RuntimeError( + "Last dimension of left and right boundary polylines must match." + ) - polygon: NDArrayFloat = np.vstack([right_lane_bounds, left_lane_bounds[::-1], right_lane_bounds[0]]) + polygon: NDArrayFloat = np.vstack( + [right_lane_bounds, left_lane_bounds[::-1], right_lane_bounds[0]] + ) if not polygon.ndim == 2 or polygon.shape[1] not in [2, 3]: raise RuntimeError("Polygons must be Nx2 or Nx3 in shape.") return polygon diff --git a/src/av2/geometry/se3.py b/src/av2/geometry/se3.py index db199b97..7febb0ce 100644 --- a/src/av2/geometry/se3.py +++ b/src/av2/geometry/se3.py @@ -78,7 +78,9 @@ def inverse(self) -> SE3: Returns: instance of SE3 class, representing inverse of SE3 transformation target_SE3_src. """ - return SE3(rotation=self.rotation.T, translation=self.rotation.T.dot(-self.translation)) + return SE3( + rotation=self.rotation.T, translation=self.rotation.T.dot(-self.translation) + ) def compose(self, right_SE3: SE3) -> SE3: """Compose (right multiply) this class' transformation matrix T with another SE(3) instance. @@ -91,7 +93,9 @@ def compose(self, right_SE3: SE3) -> SE3: Returns: New instance of SE3 class. """ - chained_transform_matrix: NDArrayFloat = self.transform_matrix @ right_SE3.transform_matrix + chained_transform_matrix: NDArrayFloat = ( + self.transform_matrix @ right_SE3.transform_matrix + ) chained_SE3 = SE3( rotation=chained_transform_matrix[:3, :3], translation=chained_transform_matrix[:3, 3], diff --git a/src/av2/geometry/sim2.py b/src/av2/geometry/sim2.py index 8a8f71d4..c84c118b 100644 --- a/src/av2/geometry/sim2.py +++ b/src/av2/geometry/sim2.py @@ -14,8 +14,10 @@ import numbers from dataclasses import dataclass from pathlib import Path +from typing import Union import numpy as np +from upath import UPath import av2.utils.io as io_utils from av2.utils.helpers import assert_np_array_shape @@ -50,7 +52,9 @@ def __post_init__(self) -> None: if not isinstance(self.s, numbers.Number): raise ValueError("Scale `s` must be a numeric type!") if math.isclose(self.s, 0.0): - raise ZeroDivisionError("3x3 matrix formation would require division by zero") + raise ZeroDivisionError( + "3x3 matrix formation would require division by zero" + ) @property def theta_deg(self) -> float: @@ -71,8 +75,10 @@ def theta_deg(self) -> float: def __repr__(self) -> str: """Return a human-readable string representation of the class.""" - trans = np.round(self.t, 2) # type: ignore - return f"Angle (deg.): {self.theta_deg:.1f}, Trans.: {trans}, Scale: {self.s:.1f}" + trans = np.round(self.t, 2) + return ( + f"Angle (deg.): {self.theta_deg:.1f}, Trans.: {trans}, Scale: {self.s:.1f}" + ) def __eq__(self, other: object) -> bool: """Check for equality with other Sim(2) object.""" @@ -132,7 +138,7 @@ def compose(self, S: Sim2) -> Sim2: # fmt: off return Sim2( R=self.R @ S.R, - t=self.R @ S.t + ((1.0 / S.s) * self.t), # type: ignore + t=self.R @ S.t + ((1.0 / S.s) * self.t), s=self.s * S.s ) # fmt: on @@ -187,9 +193,9 @@ def save_as_json(self, save_fpath: Path) -> None: io_utils.save_json_dict(save_fpath, dict_for_serialization) @classmethod - def from_json(cls, json_fpath: Path) -> Sim2: + def from_json(cls, json_fpath: Union[Path, UPath]) -> Sim2: """Generate class inst. from a JSON file containing Sim(2) parameters as flattened matrices (row-major).""" - with open(json_fpath, "r") as f: + with json_fpath.open("r") as f: json_data = json.load(f) R: NDArrayFloat = np.array(json_data["R"]).reshape(2, 2) @@ -201,7 +207,9 @@ def from_json(cls, json_fpath: Path) -> Sim2: def from_matrix(cls, T: NDArrayFloat) -> Sim2: """Generate class instance from a 3x3 Numpy matrix.""" if np.isclose(T[2, 2], 0.0): - raise ZeroDivisionError("Sim(2) scale calculation would lead to division by zero.") + raise ZeroDivisionError( + "Sim(2) scale calculation would lead to division by zero." + ) R = T[:2, :2] t = T[:2, 2] diff --git a/src/av2/geometry/utm.py b/src/av2/geometry/utm.py index 41562e12..051206c0 100644 --- a/src/av2/geometry/utm.py +++ b/src/av2/geometry/utm.py @@ -50,7 +50,9 @@ class CityName(str, Enum): } -def convert_gps_to_utm(latitude: float, longitude: float, city_name: CityName) -> Tuple[float, float]: +def convert_gps_to_utm( + latitude: float, longitude: float, city_name: CityName +) -> Tuple[float, float]: """Convert GPS coordinates to UTM coordinates. Args: @@ -62,7 +64,13 @@ def convert_gps_to_utm(latitude: float, longitude: float, city_name: CityName) - easting: corresponding UTM Easting. northing: corresponding UTM Northing. """ - projector = Proj(proj="utm", zone=UTM_ZONE_MAP[city_name], ellps="WGS84", datum="WGS84", units="m") + projector = Proj( + proj="utm", + zone=UTM_ZONE_MAP[city_name], + ellps="WGS84", + datum="WGS84", + units="m", + ) # convert to UTM. easting, northing = projector(longitude, latitude) @@ -70,7 +78,9 @@ def convert_gps_to_utm(latitude: float, longitude: float, city_name: CityName) - return easting, northing -def convert_city_coords_to_utm(points_city: Union[NDArrayFloat, NDArrayInt], city_name: CityName) -> NDArrayFloat: +def convert_city_coords_to_utm( + points_city: Union[NDArrayFloat, NDArrayInt], city_name: CityName +) -> NDArrayFloat: """Convert city coordinates to UTM coordinates. Args: @@ -82,12 +92,18 @@ def convert_city_coords_to_utm(points_city: Union[NDArrayFloat, NDArrayInt], cit """ latitude, longitude = CITY_ORIGIN_LATLONG_DICT[city_name] # get (easting, northing) of origin - origin_utm = convert_gps_to_utm(latitude=latitude, longitude=longitude, city_name=city_name) - points_utm: NDArrayFloat = points_city.astype(float) + np.array(origin_utm, dtype=float) + origin_utm = convert_gps_to_utm( + latitude=latitude, longitude=longitude, city_name=city_name + ) + points_utm: NDArrayFloat = points_city.astype(float) + np.array( + origin_utm, dtype=float + ) return points_utm -def convert_city_coords_to_wgs84(points_city: Union[NDArrayFloat, NDArrayInt], city_name: CityName) -> NDArrayFloat: +def convert_city_coords_to_wgs84( + points_city: Union[NDArrayFloat, NDArrayInt], city_name: CityName +) -> NDArrayFloat: """Convert city coordinates to WGS84 coordinates. Args: @@ -99,7 +115,13 @@ def convert_city_coords_to_wgs84(points_city: Union[NDArrayFloat, NDArrayInt], c """ points_utm = convert_city_coords_to_utm(points_city, city_name) - projector = Proj(proj="utm", zone=UTM_ZONE_MAP[city_name], ellps="WGS84", datum="WGS84", units="m") + projector = Proj( + proj="utm", + zone=UTM_ZONE_MAP[city_name], + ellps="WGS84", + datum="WGS84", + units="m", + ) points_wgs84 = [] for easting, northing in points_utm: diff --git a/src/av2/map/README.md b/src/av2/map/README.md deleted file mode 100644 index 25a3f9b4..00000000 --- a/src/av2/map/README.md +++ /dev/null @@ -1,146 +0,0 @@ - -# Maps for Argoverse 2.0 - -## Table of Contents - -- [Overview](#overview) -- [Map Counts](#map-counts) -- [Vector Map: Lane Graph and Lane Segments](#lane-segments) -- [Vector Map: Drivable Area](#drivable-area) -- [Vector Map: Pedestrian Crossings](#ped-crossings) -- [Area of Local Maps](#area-of-local-maps) -- [Raster Maps: Ground surface height](#ground-height) -- [Training Online Map Inference Models](#training-online-map-inference-models) - -## Overview - -In all three datasets, each scenario contains its own HD Map with 3D lane, crosswalk, and drivable area geometry — sourced from data captured in six distinct cities (Austin, Detroit, Miami, Palo Alto, Pittsburgh, and Washington D.C.). - -Each scenario in the three datasets described above shares the same HD map representation. Each scenario carries its own local map region. This is a departure from the original Argoverse datasets in which all scenarios were localized onto two city-scale maps. Advantages of per-scenario maps include: - -- More efficient queries. -- The ability to handle map changes. A particular intersection might be observed multiple times in our datasets, and there could be changes to the lanes, crosswalks, or even ground height in between the times when the logs or sequences were captured. - -## Map Counts - -Argoverse 2.0 offers a massive number of highly diverse HD maps: - -- **Motion Forecasting Dataset**: ~250,000 vector maps. -- **Sensor Dataset**: 1,000 vector maps and 1,000 ground height raster maps. -- **LiDAR Dataset**: 20,000 vector maps. -- **TbV Dataset**: 1,038 vector maps and 1,038 ground height raster maps. - -The core data structure that holds Argoverse 2.0 map data is the [`ArgoverseStaticMap`](map_api.py#280) class. Please refer to the [map tutorial notebook](../../../tutorials/map_tutorial.ipynb) for more examples of how to use the map API. - - - -## Vector Map: Lane Graph and Lane Segments - -The core feature of the HD map is the lane graph, consisting of a graph G = (V, E), where V are individual lane segments. In the [supplemental material](https://openreview.net/attachment?id=vKQGe36av4k&name=supplementary_material), we enumerate and define the attributes we provide for each lane segment. Unlike Argoverse 1, we provide the actual 3D lane boundaries, instead of only centerlines. However, our API provides code to quickly infer the centerlines at any desired sampling resolution. Polylines are quantized to 1 cm resolution in the release. - -

- -

- -These vector map files are provided as JSON, e.g. `log_map_archive_00a6ffc1-6ce9-3bc3-a060-6006e9893a1a____PIT_city_31785.json` - -They may be loaded as follows: - -```python -from av2.map.map_api import ArgoverseStaticMap -log_map_dirpath = Path("av2") / "00a6ffc1-6ce9-3bc3-a060-6006e9893a1a" / "map" -avm = ArgoverseStaticMap.from_map_dir(log_map_dirpath=log_map_dirpath, build_raster=False) -``` - -Please refer to the [`LaneSegment`](lane_segment.py#L71) class, with the following attributes: - -- `id`: unique identifier for this lane segment (guaranteed to be unique only within this local map). -- `is_intersection`: boolean value representing whether or not this lane segment lies within an intersection. -- `lane_type`: designation of which vehicle types may legally utilize this lane for travel (see [`LaneType`](lane_segment.py#L23)). -- `right_lane_boundary`: 3d polyline representing the right lane boundary (see [`Polyline`](map_primitives.py#L37)). -- `left_lane_boundary`: 3d polyline representing the left lane boundary. -- `right_mark_type`: type of painted marking found along the right lane boundary (see [`LaneMarkType`](lane_segment.py#L31)). -- `left_mark_type`: type of painted marking found along the left lane boundary. -- `predecessors`: unique identifiers of lane segments that are predecessors of this object. -- `successors`: unique identifiers of lane segments that represent successor of this object. Note: this list will be empty if no successors exist. -- `right_neighbor_id`: unique identifier of the lane segment representing this object's right neighbor. -- `left_neighbor_id`: unique identifier of the lane segment representing this object's left neighbor. - -

- - -

- - - -## Vector Map: Drivable Area - -Instead of providing drivable area segmentation in a rasterized format, as we did in Argoverse 1, we release it in a vector format (i.e. as 3D polygons). This offers multiple advantages, chiefly in compression - allowing us to store separate maps for tens of thousands of scenarios, while ensuring that the raster format is still easily derivable. The polygon vertices are quantized to 1 cm resolution. - -Please refer to the [`DrivableArea`](drivable_area.py#L17) class, with the following attributes: - -- `id`: unique identifier. -- `area_boundary`: 3d vertices of polygon, representing the drivable area's boundary. - - - -## Vector Map: Pedestrian Crossings - -These entities represent crosswalks, and are provided in vector format. They are parameterized by two edges along a principal axis. Both lines should be pointing in nominally the same direction and a pedestrian is expected to move either roughly parallel to both lines or anti-parallel to both lines. - -Please refer to the [`PedestrianCrossing`](pedestrian_crossing.py#L17) class, with the following attributes: - -- `id`: unique identifier of pedestrian crossing. -- `edge1`: 3d polyline representing one edge of the crosswalk, with 2 waypoints. -- `edge2`: 3d polyline representing the other edge of the crosswalk, with 2 waypoints. - -## Area of Local Maps - -Each scenario’s local map includes all entities found within a 100 m dilation in l2-norm from the ego-vehicle trajectory. - - - -## Raster Maps: Ground Surface Height - -Only the AV 2.0 Sensor Dataset and TbV includes a dense ground surface height map. (The AV 2.0 LiDAR dataset and AV 2.0 Motion Forecasting (MF) datasets **do not** come up with raster maps, but still have sparse 3D height information on polylines). - -

- -

- -Ground surface height is provided for areas within a 5 m isocontour of the drivable area boundary, which we define as the region of interest (ROI). We do so because the notion of ground surface height is ill-defined for the interior of buildings and interior of densely constructed city blocks, areas where ground vehicles cannot observe due to occlusion. The raster grid is quantized to a 30 cm resolution, a higher resolution than the 1 m resolution in Argoverse 1. - -This data is provided as `float16` Numpy files: -`00a6ffc1-6ce9-3bc3-a060-6006e9893a1a_ground_height_surface____PIT.npy` - -**Sim(2) representation**: In order to access the raster values, a Similarity(2) mapping from city coordinates to the high-resolution raster grid (which we refer to as `img` or `array`). This Similarity(2) object provided as a JSON file per log encapsulating (R,t,s), e.g.: - -`00a6ffc1-6ce9-3bc3-a060-6006e9893a1a___img_Sim2_city.json` - -Points may be transformed as follows: - -```python -from av2.geometry.sim2 import Sim2 -array_Sim2_city = Sim2.from_json(json_fpath) -points_array = array_Sim2_city.transform_from(points_city) -``` - -However, instead of indexing manually into the raster arrays, the API will handle this for you: - -```python -avm = ArgoverseStaticMap.from_map_dir(log_map_dirpath=log_map_dirpath, build_raster=True) -points_z = avm.raster_ground_height_layer.get_ground_height_at_xy(points_xy) -``` - -## Training Online Map Inference Models - -Argoverse 2.0 offers new opportunities for training online map inference models, as the largest source of paired sensor data and HD maps publicly available at the time of release. - -However, a few Sensor 2.0 Dataset logs intentionally feature HD map changes: - -1. `75e8adad-50a6-3245-8726-5e612db3d165` -2. `54bc6dbc-ebfb-3fba-b5b3-57f88b4b79ca` -3. `af170aac-8465-3d7b-82c5-64147e94af7d` -4. `6e106cf8-f6dd-38f6-89c8-9be7a71e7275` - -These logs should not be used for training online map inference models, as the real-world scene has recently been updated via constructed, leaving the map out-of-date. diff --git a/src/av2/map/drivable_area.py b/src/av2/map/drivable_area.py index b8a46aae..5d27ba5c 100644 --- a/src/av2/map/drivable_area.py +++ b/src/av2/map/drivable_area.py @@ -33,7 +33,9 @@ def xyz(self) -> NDArrayFloat: @classmethod def from_dict(cls, json_data: Dict[str, Any]) -> DrivableArea: """Generate object instance from dictionary read from JSON data.""" - point_list = [Point(x=v["x"], y=v["y"], z=v["z"]) for v in json_data["area_boundary"]] + point_list = [ + Point(x=v["x"], y=v["y"], z=v["z"]) for v in json_data["area_boundary"] + ] # append the first vertex to the end of vertex list point_list.append(point_list[0]) diff --git a/src/av2/map/lane_segment.py b/src/av2/map/lane_segment.py index a3a25130..e6c76b07 100644 --- a/src/av2/map/lane_segment.py +++ b/src/av2/map/lane_segment.py @@ -72,7 +72,7 @@ class LocalLaneMarking: @dataclass(frozen=False) class LaneSegment: - """Vector representation of a single lane segment within a log-specific Argoverse 2.0 map. + """Vector representation of a single lane segment within a log-specific Argoverse 2 map. Args: id: unique identifier for this lane segment (guaranteed to be unique only within this local map). @@ -107,7 +107,9 @@ def from_dict(cls, json_data: Dict[str, Any]) -> LaneSegment: return cls( id=json_data["id"], lane_type=LaneType(json_data["lane_type"]), - right_lane_boundary=Polyline.from_json_data(json_data["right_lane_boundary"]), + right_lane_boundary=Polyline.from_json_data( + json_data["right_lane_boundary"] + ), left_lane_boundary=Polyline.from_json_data(json_data["left_lane_boundary"]), right_mark_type=LaneMarkType(json_data["right_lane_mark_type"]), left_mark_type=LaneMarkType(json_data["left_lane_mark_type"]), @@ -122,7 +124,10 @@ def from_dict(cls, json_data: Dict[str, Any]) -> LaneSegment: def left_lane_marking(self) -> LocalLaneMarking: """Retrieve the left lane marking associated with this lane segment.""" return LocalLaneMarking( - mark_type=self.left_mark_type, src_lane_id=self.id, bound_side="left", polyline=self.left_lane_boundary.xyz + mark_type=self.left_mark_type, + src_lane_id=self.id, + bound_side="left", + polyline=self.left_lane_boundary.xyz, ) @property @@ -146,7 +151,9 @@ def polygon_boundary(self) -> NDArrayFloat: self.right_lane_boundary.xyz, self.left_lane_boundary.xyz ) - def is_within_l_infinity_norm_radius(self, query_center: NDArrayFloat, search_radius_m: float) -> bool: + def is_within_l_infinity_norm_radius( + self, query_center: NDArrayFloat, search_radius_m: float + ) -> bool: """Whether any waypoint of lane boundaries falls within search_radius_m of query center, by l-infinity norm. Could have very long segment, with endpoints and all waypoints outside of radius, therefore cannot check just @@ -161,10 +168,12 @@ def is_within_l_infinity_norm_radius(self, query_center: NDArrayFloat, search_ra """ try: right_ln_bnd_interp = interp_utils.interp_arc( - t=WPT_INFINITY_NORM_INTERP_NUM, points=self.right_lane_boundary.xyz[:, :2] + t=WPT_INFINITY_NORM_INTERP_NUM, + points=self.right_lane_boundary.xyz[:, :2], ) left_ln_bnd_interp = interp_utils.interp_arc( - t=WPT_INFINITY_NORM_INTERP_NUM, points=self.left_lane_boundary.xyz[:, :2] + t=WPT_INFINITY_NORM_INTERP_NUM, + points=self.left_lane_boundary.xyz[:, :2], ) except Exception: logger.exception("Interpolation failed for lane segment %d", self.id) diff --git a/src/av2/map/map_api.py b/src/av2/map/map_api.py index f0436924..a512623d 100644 --- a/src/av2/map/map_api.py +++ b/src/av2/map/map_api.py @@ -1,6 +1,6 @@ # -"""API for loading and Argoverse 2.0 maps. +"""API for loading and Argoverse 2 maps. These include left and right lane boundaries, instead of only lane centerlines, as was the case in Argoverse 1.0 and 1.1. @@ -21,15 +21,16 @@ from typing import Dict, Final, List, Optional, Tuple, Union import numpy as np +from upath import UPath import av2.geometry.interpolate as interp_utils import av2.utils.dilation_utils as dilation_utils -import av2.utils.io as io_utils import av2.utils.raster as raster_utils from av2.geometry.sim2 import Sim2 from av2.map.drivable_area import DrivableArea from av2.map.lane_segment import LaneSegment from av2.map.pedestrian_crossing import PedestrianCrossing +from av2.utils import io from av2.utils.typing import NDArrayBool, NDArrayByte, NDArrayFloat, NDArrayInt # 1 meter resolution is insufficient for the online-generated drivable area and ROI raster grids @@ -92,7 +93,9 @@ def get_raster_values_at_coords( * (npyimage_coords[:, 0] >= 0) * (npyimage_coords[:, 0] < self.array.shape[1]) ) - raster_values[ind_valid_pts] = self.array[npyimage_coords[ind_valid_pts, 1], npyimage_coords[ind_valid_pts, 0]] + raster_values[ind_valid_pts] = self.array[ + npyimage_coords[ind_valid_pts, 1], npyimage_coords[ind_valid_pts, 0] + ] return raster_values @@ -105,7 +108,7 @@ class GroundHeightLayer(RasterMapLayer): """ @classmethod - def from_file(cls, log_map_dirpath: Path) -> GroundHeightLayer: + def from_file(cls, log_map_dirpath: Union[Path, UPath]) -> GroundHeightLayer: """Load ground height values (w/ values at 30 cm resolution) from .npy file, and associated Sim(2) mapping. Note: ground height values are stored on disk as a float16 2d-array, but cast to float32 once loaded for @@ -121,20 +124,27 @@ def from_file(cls, log_map_dirpath: Path) -> GroundHeightLayer: RuntimeError: If raster ground height layer file is missing or Sim(2) mapping from city to image coordinates is missing. """ - ground_height_npy_fpaths = sorted(log_map_dirpath.glob("*_ground_height_surface____*.npy")) + ground_height_npy_fpaths = sorted( + log_map_dirpath.glob("*_ground_height_surface____*.npy") + ) if not len(ground_height_npy_fpaths) == 1: raise RuntimeError("Raster ground height layer file is missing") Sim2_json_fpaths = sorted(log_map_dirpath.glob("*___img_Sim2_city.json")) if not len(Sim2_json_fpaths) == 1: - raise RuntimeError("Sim(2) mapping from city to image coordinates is missing") + raise RuntimeError( + "Sim(2) mapping from city to image coordinates is missing" + ) # load the file with rasterized values - ground_height_array: NDArrayFloat = np.load(ground_height_npy_fpaths[0]) # type: ignore + with ground_height_npy_fpaths[0].open("rb") as f: + ground_height_array: NDArrayFloat = np.load(f) array_Sim2_city = Sim2.from_json(Sim2_json_fpaths[0]) - return cls(array=ground_height_array.astype(np.float32), array_Sim2_city=array_Sim2_city) + return cls( + array=ground_height_array.astype(float), array_Sim2_city=array_Sim2_city + ) def get_ground_points_boolean(self, points_xyz: NDArrayFloat) -> NDArrayBool: """Check whether each 3d point is likely to be from the ground surface. @@ -150,11 +160,15 @@ def get_ground_points_boolean(self, points_xyz: NDArrayFloat) -> NDArrayBool: ValueError: If `points_xyz` aren't 3d. """ if points_xyz.shape[1] != 3: - raise ValueError("3-dimensional points must be provided to classify them as `ground` with the map.") + raise ValueError( + "3-dimensional points must be provided to classify them as `ground` with the map." + ) ground_height_values = self.get_ground_height_at_xy(points_xyz) z = points_xyz[:, 2] - near_ground: NDArrayBool = np.absolute(z - ground_height_values) <= GROUND_HEIGHT_THRESHOLD_M + near_ground: NDArrayBool = ( + np.absolute(z - ground_height_values) <= GROUND_HEIGHT_THRESHOLD_M + ) underground: NDArrayBool = z < ground_height_values is_ground_boolean_arr: NDArrayBool = near_ground | underground return is_ground_boolean_arr @@ -179,9 +193,9 @@ def get_ground_height_at_xy(self, points_xyz: NDArrayFloat) -> NDArrayFloat: Returns: Numpy array of shape (K,) """ - ground_height_values: NDArrayFloat = self.get_raster_values_at_coords(points_xyz, fill_value=np.nan).astype( - float - ) + ground_height_values: NDArrayFloat = self.get_raster_values_at_coords( + points_xyz, fill_value=np.nan + ).astype(float) return ground_height_values @@ -193,7 +207,9 @@ class DrivableAreaMapLayer(RasterMapLayer): """ @classmethod - def from_vector_data(cls, drivable_areas: List[DrivableArea]) -> DrivableAreaMapLayer: + def from_vector_data( + cls, drivable_areas: List[DrivableArea] + ) -> DrivableAreaMapLayer: """Return a drivable area map from vector data. NOTE: This function provides "drivable area" as a binary segmentation mask in the bird's eye view. @@ -214,14 +230,16 @@ def from_vector_data(cls, drivable_areas: List[DrivableArea]) -> DrivableAreaMap img_w = int((x_max - x_min + 1) * array_s_city) # scale determines the resolution of the raster DA layer. - array_Sim2_city = Sim2(R=np.eye(2), t=np.array([-x_min, -y_min]), s=array_s_city) + array_Sim2_city = Sim2( + R=np.eye(2), t=np.array([-x_min, -y_min]), s=array_s_city + ) # convert vertices for each polygon from a 3d array in city coordinates, to a 2d array # in image/array coordinates. da_polygons_img = [] for da_polygon_city in drivable_areas: da_polygon_img = array_Sim2_city.transform_from(da_polygon_city.xyz[:, :2]) - da_polygon_img = np.round(da_polygon_img).astype(np.int32) # type: ignore + da_polygon_img = np.round(da_polygon_img).astype(np.int32) da_polygons_img.append(da_polygon_img) da_mask = raster_utils.get_mask_from_polygons(da_polygons_img, img_h, img_w) @@ -237,7 +255,9 @@ class RoiMapLayer(RasterMapLayer): """ @classmethod - def from_drivable_area_layer(cls, drivable_area_layer: DrivableAreaMapLayer) -> RoiMapLayer: + def from_drivable_area_layer( + cls, drivable_area_layer: DrivableAreaMapLayer + ) -> RoiMapLayer: """Rasterize and return 3d vector drivable area as a 2d array, and dilate it by 5 meters, to return a ROI mask. Args: @@ -250,13 +270,19 @@ def from_drivable_area_layer(cls, drivable_area_layer: DrivableAreaMapLayer) -> p_array = array_Sim2_city * p_city """ # initialize ROI as zero-level isocontour of drivable area, and the dilate to 5-meter isocontour - roi_mat_init: NDArrayByte = copy.deepcopy(drivable_area_layer.array).astype(np.uint8) - roi_mask = dilation_utils.dilate_by_l2(roi_mat_init, dilation_thresh=ROI_ISOCONTOUR_GRID) + roi_mat_init: NDArrayByte = copy.deepcopy(drivable_area_layer.array).astype( + np.uint8 + ) + roi_mask = dilation_utils.dilate_by_l2( + roi_mat_init, dilation_thresh=ROI_ISOCONTOUR_GRID + ) return cls(array=roi_mask, array_Sim2_city=drivable_area_layer.array_Sim2_city) -def compute_data_bounds(drivable_areas: List[DrivableArea]) -> Tuple[int, int, int, int]: +def compute_data_bounds( + drivable_areas: List[DrivableArea], +) -> Tuple[int, int, int, int]: """Find the minimum and maximum coordinates along the x and y axes for a set of drivable areas. Args: @@ -311,7 +337,7 @@ class ArgoverseStaticMap: raster_ground_height_layer: Optional[GroundHeightLayer] @classmethod - def from_json(cls, static_map_path: Path) -> ArgoverseStaticMap: + def from_json(cls, static_map_path: Union[Path, UPath]) -> ArgoverseStaticMap: """Instantiate an Argoverse static map object (without raster data) from a JSON file containing map data. Args: @@ -322,17 +348,24 @@ def from_json(cls, static_map_path: Path) -> ArgoverseStaticMap: An Argoverse HD map. """ log_id = static_map_path.stem.split("log_map_archive_")[1] - vector_data = io_utils.read_json_file(static_map_path) + vector_data = io.read_json_file(static_map_path) - vector_drivable_areas = {da["id"]: DrivableArea.from_dict(da) for da in vector_data["drivable_areas"].values()} - vector_lane_segments = {ls["id"]: LaneSegment.from_dict(ls) for ls in vector_data["lane_segments"].values()} + vector_drivable_areas = { + da["id"]: DrivableArea.from_dict(da) + for da in vector_data["drivable_areas"].values() + } + vector_lane_segments = { + ls["id"]: LaneSegment.from_dict(ls) + for ls in vector_data["lane_segments"].values() + } if "pedestrian_crossings" not in vector_data: logger.error("Missing Pedestrian crossings!") vector_pedestrian_crossings = {} else: vector_pedestrian_crossings = { - pc["id"]: PedestrianCrossing.from_dict(pc) for pc in vector_data["pedestrian_crossings"].values() + pc["id"]: PedestrianCrossing.from_dict(pc) + for pc in vector_data["pedestrian_crossings"].values() } return cls( @@ -346,11 +379,13 @@ def from_json(cls, static_map_path: Path) -> ArgoverseStaticMap: ) @classmethod - def from_map_dir(cls, log_map_dirpath: Path, build_raster: bool = False) -> ArgoverseStaticMap: + def from_map_dir( + cls, log_map_dirpath: Union[Path, UPath], build_raster: bool = False + ) -> ArgoverseStaticMap: """Instantiate an Argoverse map object from data stored within a map data directory. - Note: the ground height surface file and associated coordinate mapping is not provided for the - 2.0 Motion Forecasting dataset, so `build_raster` defaults to False. If raster functionality is + Note: The ground height surface file and associated coordinate mapping is not provided for the + 2 Motion Forecasting dataset, so `build_raster` defaults to False. If raster functionality is desired, users should pass `build_raster` to True (e.g. for the Sensor Datasets and Map Change Datasets). Args: @@ -368,8 +403,10 @@ def from_map_dir(cls, log_map_dirpath: Path, build_raster: bool = False) -> Argo # Load vector map data from JSON file vector_data_fnames = sorted(log_map_dirpath.glob("log_map_archive_*.json")) if not len(vector_data_fnames) == 1: - raise RuntimeError(f"JSON file containing vector map data is missing (searched in {log_map_dirpath})") - vector_data_fname = vector_data_fnames[0] + raise RuntimeError( + f"JSON file containing vector map data is missing (searched in {log_map_dirpath})" + ) + vector_data_fname = vector_data_fnames[0].name vector_data_json_path = log_map_dirpath / vector_data_fname static_map = cls.from_json(vector_data_json_path) @@ -377,10 +414,18 @@ def from_map_dir(cls, log_map_dirpath: Path, build_raster: bool = False) -> Argo # Avoid file I/O and polygon rasterization when not needed if build_raster: - drivable_areas: List[DrivableArea] = list(static_map.vector_drivable_areas.values()) - static_map.raster_drivable_area_layer = DrivableAreaMapLayer.from_vector_data(drivable_areas=drivable_areas) - static_map.raster_roi_layer = RoiMapLayer.from_drivable_area_layer(static_map.raster_drivable_area_layer) - static_map.raster_ground_height_layer = GroundHeightLayer.from_file(log_map_dirpath) + drivable_areas: List[DrivableArea] = list( + static_map.vector_drivable_areas.values() + ) + static_map.raster_drivable_area_layer = ( + DrivableAreaMapLayer.from_vector_data(drivable_areas=drivable_areas) + ) + static_map.raster_roi_layer = RoiMapLayer.from_drivable_area_layer( + static_map.raster_drivable_area_layer + ) + static_map.raster_ground_height_layer = GroundHeightLayer.from_file( + log_map_dirpath + ) return static_map @@ -394,8 +439,10 @@ def get_scenario_vector_drivable_areas(self) -> List[DrivableArea]: """ return list(self.vector_drivable_areas.values()) - def get_lane_segment_successor_ids(self, lane_segment_id: int) -> Optional[List[int]]: - """Get lane id for the lane sucessor of the specified lane_segment_id. + def get_lane_segment_successor_ids( + self, lane_segment_id: int + ) -> Optional[List[int]]: + """Get lane id for the lane successor of the specified lane_segment_id. Args: lane_segment_id: unique identifier for a lane segment within a log scenario map (within a single city). @@ -446,8 +493,12 @@ def get_lane_segment_centerline(self, lane_segment_id: int) -> NDArrayFloat: Returns: Numpy array of shape (N,3). """ - left_ln_bound = self.vector_lane_segments[lane_segment_id].left_lane_boundary.xyz - right_ln_bound = self.vector_lane_segments[lane_segment_id].right_lane_boundary.xyz + left_ln_bound = self.vector_lane_segments[ + lane_segment_id + ].left_lane_boundary.xyz + right_ln_bound = self.vector_lane_segments[ + lane_segment_id + ].right_lane_boundary.xyz lane_centerline, _ = interp_utils.compute_midpoint_line( left_ln_boundary=left_ln_bound, @@ -486,7 +537,9 @@ def get_scenario_ped_crossings(self) -> List[PedestrianCrossing]: """ return list(self.vector_pedestrian_crossings.values()) - def get_nearby_ped_crossings(self, query_center: NDArrayFloat, search_radius_m: float) -> List[PedestrianCrossing]: + def get_nearby_ped_crossings( + self, query_center: NDArrayFloat, search_radius_m: float + ) -> List[PedestrianCrossing]: """Return nearby pedestrian crossings. Returns pedestrian crossings for which any waypoint of their boundary falls within `search_radius_m` meters @@ -511,7 +564,9 @@ def get_scenario_lane_segments(self) -> List[LaneSegment]: """ return list(self.vector_lane_segments.values()) - def get_nearby_lane_segments(self, query_center: NDArrayFloat, search_radius_m: float) -> List[LaneSegment]: + def get_nearby_lane_segments( + self, query_center: NDArrayFloat, search_radius_m: float + ) -> List[LaneSegment]: """Return the nearby lane segments. Return lane segments for which any waypoint of their lane boundaries falls @@ -526,7 +581,9 @@ def get_nearby_lane_segments(self, query_center: NDArrayFloat, search_radius_m: """ scenario_lane_segments = self.get_scenario_lane_segments() return [ - ls for ls in scenario_lane_segments if ls.is_within_l_infinity_norm_radius(query_center, search_radius_m) + ls + for ls in scenario_lane_segments + if ls.is_within_l_infinity_norm_radius(query_center, search_radius_m) ] def remove_ground_surface(self, points_xyz: NDArrayFloat) -> NDArrayFloat: @@ -573,7 +630,9 @@ def remove_non_drivable_area_points(self, points_xyz: NDArrayFloat) -> NDArrayFl Returns: subset of original point cloud, returning only those points lying within the drivable area. """ - is_da_boolean_arr = self.get_raster_layer_points_boolean(points_xyz, layer_name=RasterLayerType.DRIVABLE_AREA) + is_da_boolean_arr = self.get_raster_layer_points_boolean( + points_xyz, layer_name=RasterLayerType.DRIVABLE_AREA + ) filtered_points_xyz: NDArrayFloat = points_xyz[is_da_boolean_arr] return filtered_points_xyz @@ -588,7 +647,9 @@ def remove_non_roi_points(self, points_xyz: NDArrayFloat) -> NDArrayFloat: Returns: subset of original point cloud, returning only those points lying within the ROI. """ - is_da_boolean_arr = self.get_raster_layer_points_boolean(points_xyz, layer_name=RasterLayerType.ROI) + is_da_boolean_arr = self.get_raster_layer_points_boolean( + points_xyz, layer_name=RasterLayerType.ROI + ) filtered_points_xyz: NDArrayFloat = points_xyz[is_da_boolean_arr] return filtered_points_xyz @@ -607,8 +668,13 @@ def get_rasterized_drivable_area(self) -> Tuple[NDArrayByte, Sim2]: if self.raster_drivable_area_layer is None: raise ValueError("Raster drivable area is not loaded!") - raster_drivable_area_layer: NDArrayByte = self.raster_drivable_area_layer.array.astype(np.uint8) - return raster_drivable_area_layer, self.raster_drivable_area_layer.array_Sim2_city + raster_drivable_area_layer: NDArrayByte = ( + self.raster_drivable_area_layer.array.astype(np.uint8) + ) + return ( + raster_drivable_area_layer, + self.raster_drivable_area_layer.array_Sim2_city, + ) def get_rasterized_roi(self) -> Tuple[NDArrayByte, Sim2]: """Get the drivable area along with Sim(2) that maps matrix coordinates to city coordinates. @@ -627,7 +693,9 @@ def get_rasterized_roi(self) -> Tuple[NDArrayByte, Sim2]: raster_roi_layer: NDArrayByte = self.raster_roi_layer.array.astype(np.uint8) return raster_roi_layer, self.raster_roi_layer.array_Sim2_city - def get_raster_layer_points_boolean(self, points_xyz: NDArrayFloat, layer_name: RasterLayerType) -> NDArrayBool: + def get_raster_layer_points_boolean( + self, points_xyz: NDArrayFloat, layer_name: RasterLayerType + ) -> NDArrayBool: """Query the binary segmentation layers (drivable area and ROI) at specific coordinates, to check values. Args: @@ -646,18 +714,24 @@ def get_raster_layer_points_boolean(self, points_xyz: NDArrayFloat, layer_name: if layer_name == RasterLayerType.ROI: if self.raster_roi_layer is None: raise ValueError("Raster ROI is not loaded!") - layer_values = self.raster_roi_layer.get_raster_values_at_coords(points_xyz, fill_value=0) + layer_values = self.raster_roi_layer.get_raster_values_at_coords( + points_xyz, fill_value=0 + ) elif layer_name == RasterLayerType.DRIVABLE_AREA: if self.raster_drivable_area_layer is None: raise ValueError("Raster drivable area is not loaded!") - layer_values = self.raster_drivable_area_layer.get_raster_values_at_coords(points_xyz, fill_value=0) + layer_values = self.raster_drivable_area_layer.get_raster_values_at_coords( + points_xyz, fill_value=0 + ) else: raise ValueError("layer_name should be either `roi` or `drivable_area`.") is_layer_boolean_arr: NDArrayBool = layer_values == 1.0 return is_layer_boolean_arr - def append_height_to_2d_city_pt_cloud(self, points_xy: NDArrayFloat) -> NDArrayFloat: + def append_height_to_2d_city_pt_cloud( + self, points_xy: NDArrayFloat + ) -> NDArrayFloat: """Accept 2d point cloud in xy plane and returns a 3d point cloud (xyz) by querying map for ground height. Args: diff --git a/src/av2/map/pedestrian_crossing.py b/src/av2/map/pedestrian_crossing.py index 9eabbada..271da307 100644 --- a/src/av2/map/pedestrian_crossing.py +++ b/src/av2/map/pedestrian_crossing.py @@ -44,7 +44,9 @@ def __eq__(self, other: object) -> bool: if not isinstance(other, PedestrianCrossing): return False - return np.allclose(self.edge1.xyz, other.edge1.xyz) and np.allclose(self.edge2.xyz, other.edge2.xyz) + return np.allclose(self.edge1.xyz, other.edge1.xyz) and np.allclose( + self.edge2.xyz, other.edge2.xyz + ) @classmethod def from_dict(cls, json_data: Dict[str, Any]) -> PedestrianCrossing: diff --git a/src/av2/rendering/color.py b/src/av2/rendering/color.py index f5b62ba7..d4ecfbf4 100644 --- a/src/av2/rendering/color.py +++ b/src/av2/rendering/color.py @@ -51,7 +51,7 @@ def create_range_map(points_xyz: NDArrayFloat) -> NDArrayByte: (N,3) RGB colormap. """ range = points_xyz[..., 2] - range = np.round(range).astype(int) # type: ignore + range = np.round(range).astype(int) color = plt.get_cmap("turbo")(np.arange(0, range.max() + 1)) color = color[range] range_cmap: NDArrayByte = (color * 255.0).astype(np.uint8) @@ -69,6 +69,8 @@ def create_colormap(color_list: Sequence[str], n_colors: int) -> NDArrayFloat: array of shape (n_colors, 3) representing a list of RGB colors in [0,1] """ cmap = LinearSegmentedColormap.from_list(name="dummy_name", colors=color_list) - colorscale: NDArrayFloat = np.array([cmap(k * 1 / n_colors) for k in range(n_colors)]) + colorscale: NDArrayFloat = np.array( + [cmap(k * 1 / n_colors) for k in range(n_colors)] + ) # ignore the 4th alpha channel - return colorscale[:, :3] # type: ignore + return colorscale[:, :3] diff --git a/src/av2/rendering/map.py b/src/av2/rendering/map.py index 50c898ec..1768e23d 100644 --- a/src/av2/rendering/map.py +++ b/src/av2/rendering/map.py @@ -19,7 +19,12 @@ from av2.geometry.se3 import SE3 from av2.map.lane_segment import LaneMarkType, LaneSegment from av2.map.map_api import ArgoverseStaticMap -from av2.rendering.color import HANDICAP_BLUE_BGR, RED_BGR, TRAFFIC_YELLOW1_BGR, WHITE_BGR +from av2.rendering.color import ( + HANDICAP_BLUE_BGR, + RED_BGR, + TRAFFIC_YELLOW1_BGR, + WHITE_BGR, +) from av2.utils.typing import NDArrayBool, NDArrayByte, NDArrayFloat, NDArrayInt # apply uniform dashes. in reality, there is often a roughly 2:1 ratio between empty space and dashes. @@ -93,8 +98,14 @@ def render_lane_boundary_egoview( else: bound_color = RED_BGR - if ("DOUBLE" in mark_type) or ("SOLID_DASH" in mark_type) or ("DASH_SOLID" in mark_type): - left, right = polyline_utils.get_double_polylines(polyline_city_frame, width_scaling_factor=0.1) + if ( + ("DOUBLE" in mark_type) + or ("SOLID_DASH" in mark_type) + or ("DASH_SOLID" in mark_type) + ): + left, right = polyline_utils.get_double_polylines( + polyline_city_frame, width_scaling_factor=0.1 + ) if mark_type in [ LaneMarkType.SOLID_WHITE, @@ -102,19 +113,39 @@ def render_lane_boundary_egoview( LaneMarkType.SOLID_BLUE, LaneMarkType.NONE, ]: - self.render_polyline_egoview(polyline_city_frame, img_bgr, bound_color, thickness_px=line_width_px) + self.render_polyline_egoview( + polyline_city_frame, img_bgr, bound_color, thickness_px=line_width_px + ) - elif mark_type in [LaneMarkType.DOUBLE_DASH_YELLOW, LaneMarkType.DOUBLE_DASH_WHITE]: + elif mark_type in [ + LaneMarkType.DOUBLE_DASH_YELLOW, + LaneMarkType.DOUBLE_DASH_WHITE, + ]: self.draw_dashed_polyline_egoview( - left, img_bgr, bound_color, thickness_px=line_width_px, dash_interval_m=DASH_INTERVAL_M + left, + img_bgr, + bound_color, + thickness_px=line_width_px, + dash_interval_m=DASH_INTERVAL_M, ) self.draw_dashed_polyline_egoview( - right, img_bgr, bound_color, thickness_px=line_width_px, dash_interval_m=DASH_INTERVAL_M + right, + img_bgr, + bound_color, + thickness_px=line_width_px, + dash_interval_m=DASH_INTERVAL_M, ) - elif mark_type in [LaneMarkType.DOUBLE_SOLID_YELLOW, LaneMarkType.DOUBLE_SOLID_WHITE]: - self.render_polyline_egoview(left, img_bgr, bound_color, thickness_px=line_width_px) - self.render_polyline_egoview(right, img_bgr, bound_color, thickness_px=line_width_px) + elif mark_type in [ + LaneMarkType.DOUBLE_SOLID_YELLOW, + LaneMarkType.DOUBLE_SOLID_WHITE, + ]: + self.render_polyline_egoview( + left, img_bgr, bound_color, thickness_px=line_width_px + ) + self.render_polyline_egoview( + right, img_bgr, bound_color, thickness_px=line_width_px + ) elif mark_type in [LaneMarkType.DASHED_WHITE, LaneMarkType.DASHED_YELLOW]: self.draw_dashed_polyline_egoview( @@ -125,21 +156,35 @@ def render_lane_boundary_egoview( dash_interval_m=DASH_INTERVAL_M, ) - elif (mark_type in [LaneMarkType.SOLID_DASH_WHITE, LaneMarkType.SOLID_DASH_YELLOW] and side == "right") or ( - mark_type == LaneMarkType.DASH_SOLID_YELLOW and side == "left" - ): - self.render_polyline_egoview(left, img_bgr, bound_color, thickness_px=line_width_px) + elif ( + mark_type in [LaneMarkType.SOLID_DASH_WHITE, LaneMarkType.SOLID_DASH_YELLOW] + and side == "right" + ) or (mark_type == LaneMarkType.DASH_SOLID_YELLOW and side == "left"): + self.render_polyline_egoview( + left, img_bgr, bound_color, thickness_px=line_width_px + ) self.draw_dashed_polyline_egoview( - right, img_bgr, bound_color, thickness_px=line_width_px, dash_interval_m=DASH_INTERVAL_M + right, + img_bgr, + bound_color, + thickness_px=line_width_px, + dash_interval_m=DASH_INTERVAL_M, ) - elif (mark_type in [LaneMarkType.SOLID_DASH_WHITE, LaneMarkType.SOLID_DASH_YELLOW] and side == "left") or ( - mark_type == LaneMarkType.DASH_SOLID_YELLOW and side == "right" - ): + elif ( + mark_type in [LaneMarkType.SOLID_DASH_WHITE, LaneMarkType.SOLID_DASH_YELLOW] + and side == "left" + ) or (mark_type == LaneMarkType.DASH_SOLID_YELLOW and side == "right"): self.draw_dashed_polyline_egoview( - left, img_bgr, bound_color, thickness_px=line_width_px, dash_interval_m=DASH_INTERVAL_M + left, + img_bgr, + bound_color, + thickness_px=line_width_px, + dash_interval_m=DASH_INTERVAL_M, + ) + self.render_polyline_egoview( + right, img_bgr, bound_color, thickness_px=line_width_px ) - self.render_polyline_egoview(right, img_bgr, bound_color, thickness_px=line_width_px) else: raise ValueError(f"Unknown marking type {mark_type}") @@ -169,9 +214,13 @@ def draw_dashed_polyline_egoview( dash_frequency: for each dash_interval_m, we will discretize the length into n sections. 1 of n sections will contain a marked dash, and the other (n-1) spaces will be empty (non-marked). """ - interp_polyline, num_waypts = polyline_utils.interp_polyline_by_fixed_waypt_interval(polyline, dash_interval_m) + ( + interp_polyline, + num_waypts, + ) = polyline_utils.interp_polyline_by_fixed_waypt_interval( + polyline, dash_interval_m + ) for i in range(num_waypts - 1): - # every other segment is a gap # (first the next dash interval is a line, and then the dash interval is empty, ...) if (i % dash_frequency) != 0: @@ -202,21 +251,29 @@ def render_polyline_egoview( thickness_px: thickness (in pixels) to use when rendering the polyline. """ # must use interpolated, because otherwise points may lie behind camera, etc, cannot draw - interp_polyline_city = interp_utils.interp_arc(t=N_INTERP_PTS, points=polyline_city_frame) - polyline_ego_frame = self.ego_SE3_city.transform_point_cloud(interp_polyline_city) + interp_polyline_city = interp_utils.interp_arc( + t=N_INTERP_PTS, points=polyline_city_frame + ) + polyline_ego_frame = self.ego_SE3_city.transform_point_cloud( + interp_polyline_city + ) # no need to motion compensate, since these are points originally from the city frame. - uv, points_cam, is_valid_points = self.pinhole_cam.project_ego_to_img(polyline_ego_frame) + uv, points_cam, is_valid_points = self.pinhole_cam.project_ego_to_img( + polyline_ego_frame + ) if is_valid_points.sum() == 0: return - u: NDArrayInt = np.round(uv[:, 0][is_valid_points]).astype(np.int32) # type: ignore - v: NDArrayInt = np.round(uv[:, 1][is_valid_points]).astype(np.int32) # type: ignore + u: NDArrayInt = np.round(uv[:, 0][is_valid_points]).astype(np.int32) + v: NDArrayInt = np.round(uv[:, 1][is_valid_points]).astype(np.int32) lane_z = points_cam[:, 2][is_valid_points] if self.depth_map is not None: - allowed_noise = depth_map_utils.compute_allowed_noise_per_point(points_cam[is_valid_points]) + allowed_noise = depth_map_utils.compute_allowed_noise_per_point( + points_cam[is_valid_points] + ) not_occluded = lane_z <= self.depth_map[v, u] + allowed_noise else: not_occluded = np.ones(lane_z.shape, dtype=bool) @@ -252,9 +309,8 @@ def draw_visible_polyline_segments_cv2( color: Tuple of shape (3,) with a BGR format color thickness_px: thickness (in pixels) to use when rendering the polyline. """ - line_segments_arr_int: NDArrayInt = np.round(line_segments_arr).astype(int) # type: ignore + line_segments_arr_int: NDArrayInt = np.round(line_segments_arr).astype(int) for i in range(len(line_segments_arr_int) - 1): - if (not valid_pts_bool[i]) or (not valid_pts_bool[i + 1]): continue @@ -264,4 +320,11 @@ def draw_visible_polyline_segments_cv2( y2 = line_segments_arr_int[i + 1][1] # Use anti-aliasing (AA) for curves - image = cv2.line(image, pt1=(x1, y1), pt2=(x2, y2), color=color, thickness=thickness_px, lineType=cv2.LINE_AA) + image = cv2.line( + image, + pt1=(x1, y1), + pt2=(x2, y2), + color=color, + thickness=thickness_px, + lineType=cv2.LINE_AA, + ) diff --git a/src/av2/rendering/ops/draw.py b/src/av2/rendering/ops/draw.py index 11c845d6..5457ffcc 100644 --- a/src/av2/rendering/ops/draw.py +++ b/src/av2/rendering/ops/draw.py @@ -9,14 +9,16 @@ import numpy as np from av2.utils.constants import NAN -from av2.utils.typing import NDArrayByte, NDArrayFloat, NDArrayInt, NDArrayNumber +from av2.utils.typing import NDArrayByte, NDArrayFloat, NDArrayInt -UINT8_MAX: Final[np.uint8] = np.iinfo(np.uint8).max +UINT8_MAX: Final[np.uint8] = np.uint8(np.iinfo(np.uint8).max) UINT8_BITS: Final[np.uint8] = np.log2(UINT8_MAX + 1).astype(np.uint8) @nb.njit -def integer_linear_interpolation(x: np.uint8, y: np.uint8, alpha: np.uint16, beta: np.uint16) -> np.uint8: +def integer_linear_interpolation( + x: np.uint8, y: np.uint8, alpha: np.uint16, beta: np.uint16 +) -> np.uint8: """Approximate floating point linear interpolation. This function approximates the following: @@ -43,7 +45,9 @@ def integer_linear_interpolation(x: np.uint8, y: np.uint8, alpha: np.uint16, bet @nb.njit -def alpha_blend_kernel(fg: NDArrayByte, bg: NDArrayByte, alpha: np.uint8) -> Tuple[np.uint8, np.uint8, np.uint8]: +def alpha_blend_kernel( + fg: NDArrayByte, bg: NDArrayByte, alpha: np.uint8 +) -> Tuple[np.uint8, np.uint8, np.uint8]: """Fast integer alpha blending. Reference: https://stackoverflow.com/a/12016968 @@ -127,10 +131,14 @@ def draw_points_kernel( if (vj >= 0 and uk >= 0) and (vj < H and uk < W): alpha_vj_uk = alpha if with_anti_alias: - alpha_vj_uk *= gaussian_kernel(vj, v, sigma) * gaussian_kernel(uk, u, sigma) + alpha_vj_uk *= gaussian_kernel(vj, v, sigma) * gaussian_kernel( + uk, u, sigma + ) alpha_vj_uk *= float(UINT8_MAX) alpha_vj_uk_uint8 = np.uint8(alpha_vj_uk) - blend = alpha_blend_kernel(colors[i], img[vj, uk], alpha=alpha_vj_uk_uint8) + blend = alpha_blend_kernel( + colors[i], img[vj, uk], alpha=alpha_vj_uk_uint8 + ) img[vj, uk, 0] = blend[0] img[vj, uk, 1] = blend[1] img[vj, uk, 2] = blend[2] @@ -138,7 +146,9 @@ def draw_points_kernel( @nb.njit(nogil=True) -def clip_line_frustum(p1: NDArrayNumber, p2: NDArrayNumber, planes: NDArrayFloat) -> NDArrayFloat: +def clip_line_frustum( + p1: NDArrayFloat, p2: NDArrayFloat, planes: NDArrayFloat +) -> NDArrayFloat: """Iterate over the frustum planes and intersect them with the segment. We exploit the fact that in a camera frustum, all plane normals point inside the frustum volume. diff --git a/src/av2/rendering/rasterize.py b/src/av2/rendering/rasterize.py index 746ba82b..81e358cf 100644 --- a/src/av2/rendering/rasterize.py +++ b/src/av2/rendering/rasterize.py @@ -54,13 +54,13 @@ def xyz_to_bev( # If only xyz are provided, then assume intensity is 1.0. # Otherwise, use the provided intensity. + intensity: NDArrayByte if xyz.shape[-1] == 3: - intensity: NDArrayByte = np.ones_like(xyz.shape[0], np.uint8) + cart = xyz.copy() + intensity = np.ones_like(xyz[:, 0:1], np.uint8) else: - intensity = xyz[..., -1].copy() - - # Grab the Cartesian coordinates (xyz). - cart = xyz[..., :-1].copy() + cart = xyz[..., :-1].copy() + intensity = xyz[..., -1].copy().astype(np.uint8) # Move the origin to the center of the image. cart += np.divide(grid_size_m, 2) @@ -78,7 +78,9 @@ def xyz_to_bev( # Crop point cloud to the region-of-interest. lower_bound_inclusive = (0.0, 0.0, 0.0) indices_cropped, grid_boundary_reduction = crop_points( - indices, lower_bound_inclusive=lower_bound_inclusive, upper_bound_exclusive=grid_size_m + indices, + lower_bound_inclusive=lower_bound_inclusive, + upper_bound_exclusive=grid_size_m, ) # Filter the indices and intensity values. @@ -100,8 +102,10 @@ def xyz_to_bev( img[u, v, :3] = cmap[i] img[u, v, 3:4] += intensity[i] - # Normalize the intensity. - img[..., -1] = img[..., -1] / img[..., -1].max() + normalization = img[..., -1].max() + if normalization != 0: + # Normalize the intensity. + img[..., -1] = img[..., -1] / normalization # Gamma correction. img[..., -1] = np.power(img[..., -1], 0.05) diff --git a/src/av2/rendering/vector.py b/src/av2/rendering/vector.py index 8ea7edac..1b374a7f 100644 --- a/src/av2/rendering/vector.py +++ b/src/av2/rendering/vector.py @@ -65,7 +65,9 @@ def plot_polygon_patch_mpl( vertices = polygon_pts[:, :2] mpath = MPath(vertices, codes) - patch = mpatches.PathPatch(mpath, facecolor=color, edgecolor=color, alpha=alpha, zorder=zorder) + patch = mpatches.PathPatch( + mpath, facecolor=color, edgecolor=color, alpha=alpha, zorder=zorder + ) ax.add_patch(patch) @@ -130,7 +132,9 @@ def draw_line_frustum( # After clipping the line segment to the view frustum in the camera coordinate frame, we obtain # the final set of line segment vertices, and then project these into the image. uv, _, _ = cam_model.project_cam_to_img(clipped_points) - uv_int: NDArrayInt = np.round(uv).astype(int) # type: ignore + uv_int: NDArrayInt = np.round(uv).astype(int) p1, p2 = uv_int[0], uv_int[1] - img = draw_line_in_img(img, p1, p2, color=color, thickness=thickness, line_type=line_type) + img = draw_line_in_img( + img, p1, p2, color=color, thickness=thickness, line_type=line_type + ) return img diff --git a/src/av2/rendering/video.py b/src/av2/rendering/video.py index eab48e97..e013663c 100644 --- a/src/av2/rendering/video.py +++ b/src/av2/rendering/video.py @@ -35,7 +35,9 @@ class VideoCodecs(str, Enum): HEVC_VIDEOTOOLBOX = "hevc_videotoolbox" # macOS GPU acceleration. -HIGH_EFFICIENCY_VIDEO_CODECS: Final[Set[VideoCodecs]] = set([VideoCodecs.LIBX265, VideoCodecs.HEVC_VIDEOTOOLBOX]) +HIGH_EFFICIENCY_VIDEO_CODECS: Final[Set[VideoCodecs]] = set( + [VideoCodecs.LIBX265, VideoCodecs.HEVC_VIDEOTOOLBOX] +) def tile_cameras( @@ -80,32 +82,46 @@ def tile_cameras( if "ring_front_center" in named_sensors: ring_front_center = named_sensors["ring_front_center"] - tiled_im_bgr[:landscape_width, landscape_width : landscape_width + landscape_height] = ring_front_center + tiled_im_bgr[ + :landscape_width, landscape_width : landscape_width + landscape_height + ] = ring_front_center if "ring_front_right" in named_sensors: ring_front_right = named_sensors["ring_front_right"] - tiled_im_bgr[:landscape_height, landscape_width + landscape_height :] = ring_front_right + tiled_im_bgr[:landscape_height, landscape_width + landscape_height :] = ( + ring_front_right + ) if "ring_side_left" in named_sensors: ring_side_left = named_sensors["ring_side_left"] - tiled_im_bgr[landscape_height : 2 * landscape_height, :landscape_width] = ring_side_left + tiled_im_bgr[landscape_height : 2 * landscape_height, :landscape_width] = ( + ring_side_left + ) if "ring_side_right" in named_sensors: ring_side_right = named_sensors["ring_side_right"] - tiled_im_bgr[landscape_height : 2 * landscape_height, landscape_width + landscape_height :] = ring_side_right + tiled_im_bgr[ + landscape_height : 2 * landscape_height, + landscape_width + landscape_height :, + ] = ring_side_right if bev_img is not None: tiled_im_bgr[ - landscape_width : 2 * landscape_width, landscape_width : landscape_width + landscape_height + landscape_width : 2 * landscape_width, + landscape_width : landscape_width + landscape_height, ] = bev_img if "ring_rear_left" in named_sensors: ring_rear_left = named_sensors["ring_rear_left"] - tiled_im_bgr[2 * landscape_height : 3 * landscape_height, :landscape_width] = ring_rear_left + tiled_im_bgr[2 * landscape_height : 3 * landscape_height, :landscape_width] = ( + ring_rear_left + ) if "ring_rear_right" in named_sensors: ring_rear_right = named_sensors["ring_rear_right"] - tiled_im_bgr[2 * landscape_height : 3 * landscape_height, width - landscape_width :] = ring_rear_right + tiled_im_bgr[ + 2 * landscape_height : 3 * landscape_height, width - landscape_width : + ] = ring_rear_right return tiled_im_bgr diff --git a/src/av2/structures/cuboid.py b/src/av2/structures/cuboid.py index c23640d6..c9fe567b 100644 --- a/src/av2/structures/cuboid.py +++ b/src/av2/structures/cuboid.py @@ -20,7 +20,13 @@ from av2.rendering.color import BLUE_BGR, TRAFFIC_YELLOW1_BGR from av2.rendering.vector import draw_line_frustum from av2.utils.io import read_feather -from av2.utils.typing import NDArrayBool, NDArrayByte, NDArrayFloat, NDArrayInt, NDArrayObject +from av2.utils.typing import ( + NDArrayBool, + NDArrayByte, + NDArrayFloat, + NDArrayInt, + NDArrayObject, +) ORDERED_CUBOID_COL_NAMES: Final[List[str]] = [ "tx_m", @@ -104,12 +110,16 @@ def vertices_m(self) -> NDArrayFloat: # Transform unit polygons. vertices_obj_xyz_m: NDArrayFloat = (dims_lwh_m / 2.0) * unit_vertices_obj_xyz_m - vertices_dst_xyz_m = self.dst_SE3_object.transform_point_cloud(vertices_obj_xyz_m) + vertices_dst_xyz_m = self.dst_SE3_object.transform_point_cloud( + vertices_obj_xyz_m + ) # Finally, return the polygons. return vertices_dst_xyz_m - def compute_interior_points(self, points_xyz_m: NDArrayFloat) -> Tuple[NDArrayFloat, NDArrayBool]: + def compute_interior_points( + self, points_xyz_m: NDArrayFloat + ) -> Tuple[NDArrayFloat, NDArrayBool]: """Given a query point cloud, filter to points interior to the cuboid, and provide mask. Note: comparison is to cuboid vertices in the destination reference frame. @@ -148,7 +158,10 @@ def transform(self, target_SE3_dst: SE3) -> Cuboid: @classmethod def from_numpy( - cls, params: NDArrayObject, category: Optional[Enum] = None, timestamp_ns: Optional[int] = None + cls, + params: NDArrayObject, + category: Optional[Enum] = None, + timestamp_ns: Optional[int] = None, ) -> Cuboid: """Convert a set of cuboid parameters to a `Cuboid` object. @@ -162,9 +175,9 @@ def from_numpy( Returns: Constructed cuboid. """ - translation = params[:3] - length_m, width_m, height_m = params[3:6] - quat_wxyz = params[6:10] + translation: NDArrayFloat = params[:3].astype(float) + length_m, width_m, height_m = params[3:6].astype(float) + quat_wxyz = params[6:10].astype(float) rotation = geometry_utils.quat_to_mat(quat_wxyz) ego_SE3_object = SE3(rotation=rotation, translation=translation) @@ -191,13 +204,17 @@ class CuboidList: @property def xyz_center_m(self) -> NDArrayFloat: """Cartesian coordinate centers (x,y,z) in the destination reference frame.""" - center_xyz_m: NDArrayFloat = np.stack([cuboid.dst_SE3_object.translation for cuboid in self.cuboids]) + center_xyz_m: NDArrayFloat = np.stack( + [cuboid.dst_SE3_object.translation for cuboid in self.cuboids] + ) return center_xyz_m @property def dims_lwh_m(self) -> NDArrayFloat: """Object extents (length,width,height) along the (x,y,z) axes respectively in meters.""" - dims_lwh: NDArrayFloat = np.stack([cuboid.dims_lwh_m for cuboid in self.cuboids]) + dims_lwh: NDArrayFloat = np.stack( + [cuboid.dims_lwh_m for cuboid in self.cuboids] + ) return dims_lwh @cached_property @@ -219,7 +236,9 @@ def vertices_m(self) -> NDArrayFloat: Returns: (N,8,3) array of cuboid vertices. """ - vertices_m: NDArrayFloat = np.stack([cuboid.vertices_m for cuboid in self.cuboids]) + vertices_m: NDArrayFloat = np.stack( + [cuboid.vertices_m for cuboid in self.cuboids] + ) return vertices_m @property @@ -244,7 +263,9 @@ def __getitem__(self, idx: int) -> Cuboid: IndexError: if index is invalid (i.e. out of bounds). """ if idx < 0 or idx >= len(self): - raise IndexError(f"Attempted to access cuboid {idx}, but only indices [0,{len(self)-1}] are valid") + raise IndexError( + f"Attempted to access cuboid {idx}, but only indices [0,{len(self)-1}] are valid" + ) return self.cuboids[idx] def transform(self, target_SE3_dst: SE3) -> CuboidList: @@ -304,13 +325,23 @@ def project_to_cam( # Collapse first dimension to allow for vectorization. cuboids_vertices_ego = cuboids_vertices_ego.reshape(-1, D) if city_SE3_ego_cam_t is not None and city_SE3_ego_lidar_t is not None: - _, cuboids_vertices_cam, _ = cam_model.project_ego_to_img_motion_compensated( - cuboids_vertices_ego, city_SE3_ego_cam_t=city_SE3_ego_cam_t, city_SE3_ego_lidar_t=city_SE3_ego_lidar_t + ( + _, + cuboids_vertices_cam, + _, + ) = cam_model.project_ego_to_img_motion_compensated( + cuboids_vertices_ego, + city_SE3_ego_cam_t=city_SE3_ego_cam_t, + city_SE3_ego_lidar_t=city_SE3_ego_lidar_t, ) else: - _, cuboids_vertices_cam, _ = cam_model.project_ego_to_img(cuboids_vertices_ego) + _, cuboids_vertices_cam, _ = cam_model.project_ego_to_img( + cuboids_vertices_ego + ) - cuboids_vertices_cam = cuboids_vertices_cam[:, :3].reshape(N, V, D) # Unravel collapsed dimension. + cuboids_vertices_cam = cuboids_vertices_cam[:, :3].reshape( + N, V, D + ) # Unravel collapsed dimension. # Compute depth of each cuboid center (mean of the cuboid's vertices). cuboid_centers = cuboids_vertices_cam.mean(axis=1) @@ -318,7 +349,7 @@ def project_to_cam( # Sort by z-order to respect visibility in the scene. # i.e, closer objects cuboids should be drawn on top of farther objects. - z_orders: NDArrayFloat = np.argsort(-z_buffer) + z_orders: NDArrayInt = np.argsort(-z_buffer) cuboids_vertices_cam = cuboids_vertices_cam[z_orders] front_face_indices = [0, 1, 2, 3, 0] @@ -406,12 +437,16 @@ def from_dataframe(cls, data: pd.DataFrame) -> CuboidList: Returns: Constructed cuboids. """ - cuboids_parameters: NDArrayFloat = data.loc[:, ORDERED_CUBOID_COL_NAMES].to_numpy() + cuboids_parameters: NDArrayFloat = data.loc[ + :, ORDERED_CUBOID_COL_NAMES + ].to_numpy() categories: NDArrayObject = data.loc[:, "category"].to_numpy() timestamps_ns: NDArrayInt = data.loc[:, "timestamp_ns"].to_numpy() cuboid_list = [ Cuboid.from_numpy(params, category, timestamp_ns) - for params, category, timestamp_ns in zip(cuboids_parameters, categories, timestamps_ns) + for params, category, timestamp_ns in zip( + cuboids_parameters, categories, timestamps_ns + ) ] return cls(cuboid_list) diff --git a/src/av2/structures/ndgrid.py b/src/av2/structures/ndgrid.py index 0bab67d9..aebb8912 100644 --- a/src/av2/structures/ndgrid.py +++ b/src/av2/structures/ndgrid.py @@ -38,7 +38,9 @@ def __post_init__(self) -> None: the resolution is not positive. """ if not all(x < y for x, y in zip(self.min_range_m, self.max_range_m)): - raise ValueError("All minimum ranges must be less than their corresponding max ranges!") + raise ValueError( + "All minimum ranges must be less than their corresponding max ranges!" + ) if not all(x > 0 for x in self.resolution_m_per_cell): raise ValueError("Resolution per cell must be positive!") @@ -121,7 +123,10 @@ class BEVGrid(NDGrid): """ def points_to_bev_img( - self, points: NDArrayFloat, color: Tuple[int, int, int] = GRAY_BGR, diameter: int = 2 + self, + points: NDArrayFloat, + color: Tuple[int, int, int] = GRAY_BGR, + diameter: int = 2, ) -> NDArrayByte: """Convert a set of points in Cartesian space to a bird's-eye-view image. @@ -141,17 +146,23 @@ def points_to_bev_img( raise ValueError("Points must be at least 2d!") points_xy = points[..., :2].copy() # Prevent modifying input. - indices = self.transform_to_grid_coordinates(points_xy) - indices_int, _ = crop_points(indices, lower_bound_inclusive=(0.0, 0.0), upper_bound_exclusive=self.dims) + indices_int = self.transform_to_grid_coordinates(points_xy) + indices, _ = crop_points( + indices_int, + lower_bound_inclusive=(0.0, 0.0), + upper_bound_exclusive=self.dims, + ) # Construct uv coordinates. H, W = (self.dims[0], self.dims[1]) - uv = indices_int[..., :2] + uv = indices[..., :2].astype(int) C = len(color) shape = (H, W, C) img: NDArrayByte = np.zeros(shape, dtype=np.uint8) - colors: NDArrayByte = np.array([color for _ in range(len(points_xy))], dtype=np.uint8) + colors: NDArrayByte = np.array( + [color for _ in range(len(points_xy))], dtype=np.uint8 + ) img = draw_points_xy_in_img(img, uv, colors, diameter=diameter) return img diff --git a/src/av2/torch/__init__.py b/src/av2/torch/__init__.py new file mode 100644 index 00000000..f765d47c --- /dev/null +++ b/src/av2/torch/__init__.py @@ -0,0 +1,10 @@ +"""Argoverse 2 PyTorch API.""" + +from typing import Final + +LIDAR_COLUMNS: Final = ("x", "y", "z", "intensity") +QWXYZ_COLUMNS: Final = ("qw", "qx", "qy", "qz") +TRANSLATION_COLUMNS: Final = ("tx_m", "ty_m", "tz_m") +XYZLWH_QWXYZ_COLUMNS: Final = ( + TRANSLATION_COLUMNS + ("length_m", "width_m", "height_m") + QWXYZ_COLUMNS +) diff --git a/src/av2/torch/data_loaders/__init__.py b/src/av2/torch/data_loaders/__init__.py new file mode 100644 index 00000000..dd1a2b8d --- /dev/null +++ b/src/av2/torch/data_loaders/__init__.py @@ -0,0 +1 @@ +"""PyTorch sensor data-loader sub-package.""" diff --git a/src/av2/torch/data_loaders/detection.py b/src/av2/torch/data_loaders/detection.py new file mode 100644 index 00000000..290a56c2 --- /dev/null +++ b/src/av2/torch/data_loaders/detection.py @@ -0,0 +1,85 @@ +"""PyTorch data-loader for 3D object detection task.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass, field +from typing import List + +import torch +from torch.utils.data import Dataset + +import av2._r as rust +from av2.utils.typing import PathType + +from ..structures.sweep import Sweep + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@dataclass +class DetectionDataLoader(Dataset[Sweep]): # type: ignore + """PyTorch data-loader for the sensor dataset. + + The sensor dataset should exist somewhere such as `~/data/datasets/{dataset_name}/{dataset_type}/{split_name}`, + where + dataset_name = "av2", + dataset_type = "sensor, + split_name = "train". + + This data-loader backend is implemented in Rust for speed. Each iteration will yield a new sweep. + + Args: + root_dir: Path to the dataset directory. + dataset_name: Dataset name (e.g., "av2"). + split_name: Name of the dataset split (e.g., "train"). + num_accumulated_sweeps: Number of temporally accumulated sweeps (accounting for ego-vehicle motion). + memory_mapped: Boolean flag indicating whether to memory map the dataframes. + """ + + root_dir: PathType + dataset_name: str + split_name: str + num_accumulated_sweeps: int = 1 + memory_mapped: bool = False + + _backend: rust.DataLoader = field(init=False) + _current_sweep_index: int = 0 + + def __post_init__(self) -> None: + """Initialize Rust backend.""" + self._backend = rust.DataLoader( + str(self.root_dir), + self.dataset_name, + "sensor", + self.split_name, + self.num_accumulated_sweeps, + self.memory_mapped, + ) + + def __getitem__(self, sweep_index: int) -> Sweep: + """Get a sweep from the sensor dataset.""" + sweep = self._backend.get(sweep_index) + return Sweep.from_rust(sweep) + + def __len__(self) -> int: + """Length of the sensor dataset (number of sweeps).""" + return self._backend.__len__() + + def __iter__(self) -> DetectionDataLoader: + """Iterate method for the data-loader.""" + return self + + def __next__(self) -> Sweep: + """Return the next sweep.""" + if self._current_sweep_index >= self.__len__(): + raise StopIteration + datum = self.__getitem__(self._current_sweep_index) + self._current_sweep_index += 1 + return datum + + def get_synchronized_images(self, sweep_index: int) -> List[torch.Tensor]: + """Get the synchronized ring images associated with the sweep index..""" + synchronized_images_list = self._backend.get_synchronized_images(sweep_index) + return [torch.as_tensor(x) for x in synchronized_images_list] diff --git a/src/av2/torch/data_loaders/scene_flow.py b/src/av2/torch/data_loaders/scene_flow.py new file mode 100644 index 00000000..2dfe0c4e --- /dev/null +++ b/src/av2/torch/data_loaders/scene_flow.py @@ -0,0 +1,116 @@ +"""PyTorch data-loader for the scene flow task.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass, field +from functools import cached_property +from pathlib import Path +from typing import List, Optional, Tuple + +import pandas as pd +from kornia.geometry.liegroup import Se3 +from torch.utils.data import Dataset + +import av2._r as rust +from av2.map.map_api import ArgoverseStaticMap +from av2.torch.structures.flow import Flow +from av2.torch.structures.sweep import Sweep +from av2.utils.typing import PathType + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@dataclass +class SceneFlowDataloader(Dataset[Tuple[Sweep, Sweep, Se3, Optional[Flow]]]): # type: ignore + """PyTorch data-loader for the sensor dataset. + + Args: + root_dir: Path to the dataset directory. + dataset_name: Dataset name (e.g., "av2"). + split_name: Name of the dataset split (e.g., "train"). + num_accumulated_sweeps: Number of temporally accumulated sweeps (accounting for ego-vehicle motion). + memory_mapped: Boolean flag indicating whether to memory map the dataframes. + """ + + root_dir: PathType + dataset_name: str + split_name: str + num_accumulated_sweeps: int = 1 + memory_mapped: bool = False + + _backend: rust.DataLoader = field(init=False) + _current_idx: int = 0 + + def __post_init__(self) -> None: + """Initialize Rust backend.""" + self._backend = rust.DataLoader( + str(self.root_dir), + self.dataset_name, + "sensor", + self.split_name, + self.num_accumulated_sweeps, + self.memory_mapped, + ) + self.data_dir = ( + Path(self.root_dir) / self.dataset_name / "sensor" / self.split_name + ) + + @cached_property + def file_index(self) -> pd.DataFrame: + """File index dataframe composed of (log_id, timestamp_ns).""" + return self._backend.file_index.to_pandas() + + @cached_property + def index_map(self) -> List[int]: + """Create a mapping between indices in this dataloader and the underlying one.""" + indices = [] + N = self._backend.__len__() + for i in range(N): + if i + 1 < N: + next_log_id = self.file_index.loc[i + 1, "log_id"] + current_log_id = self.file_index.loc[i, "log_id"] + if current_log_id == next_log_id: + indices.append(i) + return indices + + def get_log_id(self, index: int) -> str: + """Return the log name for a given sweep index.""" + return str(self.file_index.loc[index, "log_id"]) + + def __getitem__(self, index: int) -> Tuple[Sweep, Sweep, Se3, Optional[Flow]]: + """Get a pair of sweeps, ego motion, and flow if annotations are available.""" + backend_index = self.index_map[index] + log = str(self.file_index.loc[backend_index, "log_id"]) + log_map_dirpath = self.data_dir / log / "map" + avm = ArgoverseStaticMap.from_map_dir(log_map_dirpath, build_raster=True) + + sweep = Sweep.from_rust(self._backend.get(backend_index), avm=avm) + next_sweep = Sweep.from_rust(self._backend.get(backend_index + 1), avm=avm) + + flow = None + if sweep.cuboids is not None: + flow = Flow.from_sweep_pair((sweep, next_sweep)) + + ego_1_SE3_ego_0 = next_sweep.city_SE3_ego.inverse() * sweep.city_SE3_ego + ego_1_SE3_ego_0.rotation._q.requires_grad_(False) + ego_1_SE3_ego_0.translation.requires_grad_(False) + + return sweep, next_sweep, ego_1_SE3_ego_0, flow + + def __len__(self) -> int: + """Length of the scene flow dataset (number of pairs of sweeps).""" + return len(self.index_map) + + def __iter__(self) -> SceneFlowDataloader: + """Iterate method for the data-loader.""" + return self + + def __next__(self) -> Tuple[Sweep, Sweep, Se3, Optional[Flow]]: + """Return a tuple of sweeps for scene flow.""" + if self._current_idx >= self.__len__(): + raise StopIteration + datum = self.__getitem__(self._current_idx) + self._current_idx += 1 + return datum diff --git a/src/av2/torch/structures/__init__.py b/src/av2/torch/structures/__init__.py new file mode 100644 index 00000000..91d4a29c --- /dev/null +++ b/src/av2/torch/structures/__init__.py @@ -0,0 +1 @@ +"""Argoverse 2 PyTorch structures.""" diff --git a/src/av2/torch/structures/cuboids.py b/src/av2/torch/structures/cuboids.py new file mode 100644 index 00000000..d38118e8 --- /dev/null +++ b/src/av2/torch/structures/cuboids.py @@ -0,0 +1,79 @@ +"""PyTorch Cuboids sub-module.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from functools import cached_property +from typing import List + +import pandas as pd +import torch +from kornia.geometry.conversions import euler_from_quaternion + +from .. import XYZLWH_QWXYZ_COLUMNS +from .utils import tensor_from_frame + + +class CuboidMode(str, Enum): + """Cuboid parameterization modes.""" + + XYZLWH_T = "XYZLWH_T" # 1-DOF orientation. + XYZLWH_QWXYZ = "XYZLWH_QWXYZ" # 3-DOF orientation. + + +@dataclass(frozen=True) +class Cuboids: + """Cuboid representation for objects in the scene. + + Args: + _frame: Dataframe containing the annotations and their attributes. + """ + + _frame: pd.DataFrame + + @cached_property + def category(self) -> List[str]: + """Return the object category names.""" + category_names: List[str] = self._frame["category"].to_list() + return category_names + + @cached_property + def track_uuid(self) -> List[str]: + """Return the unique track identifiers.""" + category_names: List[str] = self._frame["track_uuid"].to_list() + return category_names + + def as_tensor(self, cuboid_mode: CuboidMode = CuboidMode.XYZLWH_T) -> torch.Tensor: + """Return object cuboids as an (N,K) tensor. + + Notation: + N: Number of objects. + K: Length of the cuboid mode parameterization. + + Args: + cuboid_mode: Cuboid parameterization mode. Defaults to (N,7) tensor. + + Returns: + (N,K) torch.Tensor of cuboids with the specified cuboid_mode parameterization. + + Raises: + NotImplementedError: Raised if the cuboid mode is not supported. + """ + xyzlwh_qwxyz = tensor_from_frame(self._frame, list(XYZLWH_QWXYZ_COLUMNS)) + if cuboid_mode == CuboidMode.XYZLWH_T: + quat_wxyz = xyzlwh_qwxyz[:, 6:10] + w, x, y, z = ( + quat_wxyz[:, 0], + quat_wxyz[:, 1], + quat_wxyz[:, 2], + quat_wxyz[:, 3], + ) + _, _, yaw = euler_from_quaternion(w, x, y, z) + return torch.concat([xyzlwh_qwxyz[:, :6], yaw[:, None]], dim=-1) + elif cuboid_mode == CuboidMode.XYZLWH_QWXYZ: + return xyzlwh_qwxyz + else: + raise NotImplementedError( + f"{cuboid_mode} orientation mode is not implemented." + ) diff --git a/src/av2/torch/structures/flow.py b/src/av2/torch/structures/flow.py new file mode 100644 index 00000000..81b38d14 --- /dev/null +++ b/src/av2/torch/structures/flow.py @@ -0,0 +1,133 @@ +"""PyTorch flow sub-module.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, Final, Tuple + +import torch +from kornia.geometry.linalg import transform_points +from torch import BoolTensor, ByteTensor, FloatTensor + +from av2.evaluation.scene_flow.constants import ( + CATEGORY_TO_INDEX, + SCENE_FLOW_DYNAMIC_THRESHOLD, +) +from av2.structures.cuboid import Cuboid, CuboidList +from av2.torch.structures.cuboids import Cuboids +from av2.torch.structures.sweep import Sweep + +BOUNDING_BOX_EXPANSION: Final = 0.2 + + +@dataclass(frozen=True) +class Flow: + """Models scene flow pseudo-labels for a lidar sweep. + + Scene Flow pseudolabels come from the relative motion of all tracked objects + between two sweeps. + + + Args: + flow: (N,3) Motion vectors (x,y,z) in meters. + is_valid: (N,) 1 if the flow was successfully estimated for that point 0 otherwise + category_indices: (N,) the semantic object class of each point (0 is background) + is_dynamic: (N,) 1 if the point is considered dynamic 0 otherwise + """ + + flow: FloatTensor + is_valid: BoolTensor + category_indices: ByteTensor + is_dynamic: BoolTensor + + def __len__(self) -> int: + """Return the number of LiDAR returns in the aggregated sweep.""" + return int(self.flow.shape[0]) + + @classmethod + def from_sweep_pair(cls, sweeps: Tuple[Sweep, Sweep]) -> Flow: + """Create flow object from a pair of Sweeps. + + Args: + sweeps: Pair of sweeps to compute the flow between. + + Returns: + Flow object. + + Raises: + ValueError: If the sweeps do not have annotations loaded. + """ + current_sweep, next_sweep = (sweeps[0], sweeps[1]) + if current_sweep.cuboids is None or next_sweep.cuboids is None: + raise ValueError("Can only create flow from sweeps with annotations") + else: + current_cuboids, next_cuboids = current_sweep.cuboids, next_sweep.cuboids + city_SE3_ego0, city_SE3_ego1 = ( + current_sweep.city_SE3_ego, + next_sweep.city_SE3_ego, + ) + ego1_SE3_ego0 = city_SE3_ego1.inverse() * city_SE3_ego0 + + current_cuboid_map = cuboids_to_id_cuboid_map(current_cuboids) + next_cuboid_map = cuboids_to_id_cuboid_map(next_cuboids) + + current_pc = current_sweep.lidar.as_tensor()[:, :3] + rigid_flow = ( + (transform_points(ego1_SE3_ego0.matrix(), current_pc[None])[0] - current_pc) + .float() + .detach() + ) + flow = rigid_flow.clone() + + is_valid = torch.ones(len(current_pc), dtype=torch.bool) + category_inds = torch.zeros(len(current_pc), dtype=torch.uint8) + + for id in current_cuboid_map: + c0 = current_cuboid_map[id] + c0.length_m += BOUNDING_BOX_EXPANSION # the bounding boxes are a little too tight sometimes + c0.width_m += BOUNDING_BOX_EXPANSION + obj_pts_npy, obj_mask_npy = c0.compute_interior_points(current_pc.numpy()) + obj_pts, obj_mask = torch.as_tensor( + obj_pts_npy, dtype=torch.float32 + ), torch.as_tensor(obj_mask_npy) + category_inds[obj_mask] = CATEGORY_TO_INDEX[str(c0.category)] + + if id in next_cuboid_map: + c1 = next_cuboid_map[id] + c1_SE3_c0 = c1.dst_SE3_object.compose(c0.dst_SE3_object.inverse()) + flow[obj_mask] = ( + torch.as_tensor( + c1_SE3_c0.transform_point_cloud(obj_pts_npy), + dtype=torch.float32, + ) + - obj_pts + ) + else: + is_valid[obj_mask] = 0 + + dynamic_norm = torch.linalg.vector_norm(flow - rigid_flow, dim=-1) + is_dynamic: BoolTensor = dynamic_norm >= SCENE_FLOW_DYNAMIC_THRESHOLD + + return cls( + flow=torch.FloatTensor(flow), + is_valid=torch.BoolTensor(is_valid), + category_indices=torch.ByteTensor(category_inds), + is_dynamic=torch.BoolTensor(is_dynamic), + ) + + +def cuboids_to_id_cuboid_map(cuboids: Cuboids) -> Dict[str, Cuboid]: + """Create a mapping between track UUIDs and cuboids. + + Args: + cuboids: Cuboids to transform into a mapping. + + Returns: + A dict with the UUIDs as keys and the corresponding cuboids as values. + """ + ids = cuboids.track_uuid + annotations_df_with_ts = cuboids._frame.assign(timestamp_ns=None) + cuboid_list = CuboidList.from_dataframe(annotations_df_with_ts) + + cuboids_and_ids = dict(zip(ids, cuboid_list.cuboids)) + return cuboids_and_ids diff --git a/src/av2/torch/structures/lidar.py b/src/av2/torch/structures/lidar.py new file mode 100644 index 00000000..2ca63ecd --- /dev/null +++ b/src/av2/torch/structures/lidar.py @@ -0,0 +1,34 @@ +"""PyTorch Lidar sub-module.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Tuple + +import pandas as pd +import torch + +from av2.torch import LIDAR_COLUMNS +from av2.torch.structures.utils import tensor_from_frame + + +@dataclass(frozen=True) +class Lidar: + """Lidar sensor data structure. + + Args: + _frame: Dataframe containing lidar coordinates and features. + """ + + _frame: pd.DataFrame + + def as_tensor(self, columns: Tuple[str, ...] = LIDAR_COLUMNS) -> torch.Tensor: + """Return the lidar as a tensor with the specified columns. + + Args: + columns: List of ordered column names. + + Returns: + Tensor of lidar data. + """ + return tensor_from_frame(self._frame, list(columns)) diff --git a/src/av2/torch/structures/sweep.py b/src/av2/torch/structures/sweep.py new file mode 100644 index 00000000..2f58d0f5 --- /dev/null +++ b/src/av2/torch/structures/sweep.py @@ -0,0 +1,75 @@ +"""PyTorch sweep sub-module.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional, Tuple + +import torch +from kornia.geometry.liegroup import Se3 +from kornia.geometry.linalg import transform_points + +import av2._r as rust +from av2.map.map_api import ArgoverseStaticMap +from av2.torch.structures.lidar import Lidar + +from .cuboids import Cuboids +from .utils import SE3_from_frame + + +@dataclass(frozen=True) +class Sweep: + """Stores the annotations and lidar for one sweep. + + Notation: + N: Number of lidar points. + K: Number of lidar features. + + Args: + city_SE3_ego: Rigid transformation describing the city pose of the ego-vehicle. + lidar: Lidar sensor data. + sweep_uuid: Log id and nanosecond timestamp (unique identifier). + cuboids: Cuboids representing objects in the scene. + is_ground: Tensor of boolean values indicating which points belong to the ground. + """ + + city_SE3_ego: Se3 + lidar: Lidar + sweep_uuid: Tuple[str, int] + cuboids: Optional[Cuboids] + is_ground: Optional[torch.Tensor] = None + + @classmethod + def from_rust( + cls, sweep: rust.Sweep, avm: Optional[ArgoverseStaticMap] = None + ) -> Sweep: + """Build a sweep from the Rust backend. + + Args: + sweep: Sweep object from the Rust backend data-loader. + avm: Map object for computing ground labels + + Returns: + Sweep object. + """ + cuboids: Optional[Cuboids] = None + if sweep.cuboids is not None: + cuboids = Cuboids(_frame=sweep.cuboids.to_pandas()) + city_SE3_ego = SE3_from_frame(frame=sweep.city_pose.to_pandas()) + lidar = Lidar(sweep.lidar.to_pandas()) + + is_ground = None + if avm is not None: + pcl_ego = lidar.as_tensor()[:, :3] + pcl_city_1 = transform_points(city_SE3_ego.matrix(), pcl_ego[None])[0] + is_ground = torch.from_numpy( + avm.get_ground_points_boolean(pcl_city_1.numpy()).astype(bool) + ) + + return cls( + city_SE3_ego=city_SE3_ego, + lidar=lidar, + sweep_uuid=sweep.sweep_uuid, + is_ground=is_ground, + cuboids=cuboids, + ) diff --git a/src/av2/torch/structures/utils.py b/src/av2/torch/structures/utils.py new file mode 100644 index 00000000..4b73eb47 --- /dev/null +++ b/src/av2/torch/structures/utils.py @@ -0,0 +1,57 @@ +"""PyTorch structure utilities.""" + +from __future__ import annotations + +from typing import List + +import numpy as np +import pandas as pd +import torch +from kornia.geometry.liegroup import Se3, So3 +from kornia.geometry.quaternion import Quaternion + +from .. import QWXYZ_COLUMNS, TRANSLATION_COLUMNS + + +def tensor_from_frame(frame: pd.DataFrame, columns: List[str]) -> torch.Tensor: + """Build lidar `torch` tensor from `pandas` dataframe. + + Notation: + N: Number of rows. + K: Number of columns. + + Args: + frame: (N,K) Pandas DataFrame containing N rows with K columns. + columns: List of DataFrame columns. + + Returns: + (N,K) tensor containing the frame data. + """ + frame_npy = frame.loc[:, columns].to_numpy().astype(np.float32) + return torch.as_tensor(frame_npy) + + +def SE3_from_frame(frame: pd.DataFrame) -> Se3: + """Build SE(3) object from `pandas` DataFrame. + + Notation: + N: Number of rigid transformations. + + Args: + frame: (N,4) Pandas DataFrame containing quaternion coefficients. + + Returns: + SE(3) object representing a (N,4,4) tensor of homogeneous transformations. + """ + quaternion_npy = frame.loc[0, list(QWXYZ_COLUMNS)].to_numpy().astype(float) + quat_wxyz = Quaternion(torch.as_tensor(quaternion_npy, dtype=torch.float32)[None]) + rotation = So3(quat_wxyz) + + translation_npy = ( + frame.loc[0, list(TRANSLATION_COLUMNS)].to_numpy().astype(np.float32) + ) + translation = torch.as_tensor(translation_npy, dtype=torch.float32)[None] + dst_SE3_src = Se3(rotation, translation) + dst_SE3_src.rotation._q.requires_grad_(False) + dst_SE3_src.translation.requires_grad_(False) + return dst_SE3_src diff --git a/src/av2/utils/dataclass.py b/src/av2/utils/dataclass.py index 98a04b3c..a9d4045e 100644 --- a/src/av2/utils/dataclass.py +++ b/src/av2/utils/dataclass.py @@ -36,7 +36,10 @@ def dataclass_eq(base_dataclass: object, other: object) -> bool: # Check whether the dataclasses have equal values in all members base_tuple = vars(base_dataclass).values() other_tuple = vars(other).values() - return all(_dataclass_member_eq(base_mem, other_mem) for base_mem, other_mem in zip(base_tuple, other_tuple)) + return all( + _dataclass_member_eq(base_mem, other_mem) + for base_mem, other_mem in zip(base_tuple, other_tuple) + ) def _dataclass_member_eq(base: object, other: object) -> bool: @@ -55,7 +58,10 @@ def _dataclass_member_eq(base: object, other: object) -> bool: # If both objects are lists, check equality for all members if isinstance(base, list) and isinstance(other, list): - return all(_dataclass_member_eq(base_i, other_i) for base_i, other_i in itertools.zip_longest(base, other)) + return all( + _dataclass_member_eq(base_i, other_i) + for base_i, other_i in itertools.zip_longest(base, other) + ) # If both objects are np arrays, delegate equality check to numpy's built-in operation if isinstance(base, np.ndarray) and isinstance(other, np.ndarray): diff --git a/src/av2/utils/dense_grid_interpolation.py b/src/av2/utils/dense_grid_interpolation.py index af8ca7da..24acc3f9 100644 --- a/src/av2/utils/dense_grid_interpolation.py +++ b/src/av2/utils/dense_grid_interpolation.py @@ -45,20 +45,24 @@ def interp_dense_grid_from_sparse( """ if interp_method not in ["linear", "nearest"]: raise ValueError("Unknown interpolation method.") + if grid_img.dtype != values.dtype: + raise ValueError("Grid and values should be the same datatype.") if points.shape[0] < MIN_REQUIRED_POINTS_SIMPLEX: # return the empty grid, since we can't interpolate. return grid_img - output_dtype = values.dtype - # get (x,y) tuples back - grid_coords = mesh_grid.get_mesh_grid_as_point_cloud(min_x=0, max_x=grid_w - 1, min_y=0, max_y=grid_h - 1) + grid_coords = mesh_grid.get_mesh_grid_as_point_cloud( + min_x=0, max_x=grid_w - 1, min_y=0, max_y=grid_h - 1 + ) # make RGB a function of (dim0=x,dim1=y) - interp_vals = scipy.interpolate.griddata(points, values, grid_coords, method=interp_method) + interp_vals = scipy.interpolate.griddata( + points, values, grid_coords, method=interp_method + ) u = grid_coords[:, 0].astype(np.int32) v = grid_coords[:, 1].astype(np.int32) # Now index in at (y,x) locations grid_img[v, u] = interp_vals - return grid_img.astype(output_dtype) + return grid_img diff --git a/src/av2/utils/depth_map_utils.py b/src/av2/utils/depth_map_utils.py index 55726253..1edaead6 100644 --- a/src/av2/utils/depth_map_utils.py +++ b/src/av2/utils/depth_map_utils.py @@ -10,7 +10,9 @@ from av2.utils.typing import NDArrayByte, NDArrayFloat -MIN_DISTANCE_AWAY_M: Final[float] = 30.0 # assume max noise starting at this distance (meters) +MIN_DISTANCE_AWAY_M: Final[float] = ( + 30.0 # assume max noise starting at this distance (meters) +) MAX_ALLOWED_NOISE_M: Final[float] = 3.0 # meters @@ -29,7 +31,7 @@ def compute_allowed_noise_per_point(points_cam: NDArrayFloat) -> NDArrayFloat: Returns: array of shape (N,) representing allowed amount of noise, in meters, for entries in a depth map. """ - dists_away: NDArrayFloat = np.linalg.norm(points_cam, axis=1) # type: ignore + dists_away: NDArrayFloat = np.linalg.norm(points_cam, axis=1) max_dist_away = dists_away.max() max_dist_away = max(max_dist_away, MIN_DISTANCE_AWAY_M) @@ -39,7 +41,10 @@ def compute_allowed_noise_per_point(points_cam: NDArrayFloat) -> NDArrayFloat: def vis_depth_map( - img_rgb: NDArrayByte, depth_map: NDArrayFloat, interp_depth_map: bool, num_dilation_iters: int = 10 + img_rgb: NDArrayByte, + depth_map: NDArrayFloat, + interp_depth_map: bool, + num_dilation_iters: int = 10, ) -> None: """Visualize a depth map using Matplotlib's `inferno` colormap side by side with an RGB image. @@ -68,4 +73,4 @@ def vis_depth_map( plt.subplot(1, 2, 1) plt.imshow(img_rgb) plt.subplot(1, 2, 2) - plt.imshow((depth_map * 3).astype(np.uint8), cmap="inferno") # type: ignore + plt.imshow((depth_map * 3).astype(np.uint8), cmap="inferno") diff --git a/src/av2/utils/dilation_utils.py b/src/av2/utils/dilation_utils.py index 0b14beb3..b63d2c80 100644 --- a/src/av2/utils/dilation_utils.py +++ b/src/av2/utils/dilation_utils.py @@ -37,5 +37,7 @@ def dilate_by_l2(img: NDArrayByte, dilation_thresh: float = 5.0) -> NDArrayByte: distance_mask: NDArrayFloat = cv2.distanceTransform( mask_diff, distanceType=cv2.DIST_L2, maskSize=cv2.DIST_MASK_PRECISE ) - dilated_img: NDArrayByte = np.less_equal(distance_mask.astype(np.float32), dilation_thresh).astype(np.uint8) + dilated_img: NDArrayByte = np.less_equal( + distance_mask.astype(np.float32), dilation_thresh + ).astype(np.uint8) return dilated_img diff --git a/src/av2/utils/helpers.py b/src/av2/utils/helpers.py index 6167d000..5965b8c3 100644 --- a/src/av2/utils/helpers.py +++ b/src/av2/utils/helpers.py @@ -8,7 +8,8 @@ def assert_np_array_shape( - array: Union[NDArrayBool, NDArrayByte, NDArrayFloat, NDArrayInt], target_shape: Sequence[Optional[int]] + array: Union[NDArrayBool, NDArrayByte, NDArrayFloat, NDArrayInt], + target_shape: Sequence[Optional[int]], ) -> None: """Check for shape correctness. @@ -19,6 +20,10 @@ def assert_np_array_shape( Raises: ValueError: if array's shape does not match target_shape for any of the specified dimensions. """ - for index_dim, (array_shape_dim, target_shape_dim) in enumerate(zip(array.shape, target_shape)): + for index_dim, (array_shape_dim, target_shape_dim) in enumerate( + zip(array.shape, target_shape) + ): if target_shape_dim and array_shape_dim != target_shape_dim: - raise ValueError(f"array.shape[{index_dim}]: {array_shape_dim} != {target_shape_dim}.") + raise ValueError( + f"array.shape[{index_dim}]: {array_shape_dim} != {target_shape_dim}." + ) diff --git a/src/av2/utils/io.py b/src/av2/utils/io.py index 633f498b..74599a57 100644 --- a/src/av2/utils/io.py +++ b/src/av2/utils/io.py @@ -10,10 +10,11 @@ import numpy as np import pandas as pd from pyarrow import feather +from upath import UPath import av2.geometry.geometry as geometry_utils from av2.geometry.se3 import SE3 -from av2.utils.typing import NDArrayByte, NDArrayFloat +from av2.utils.typing import NDArrayByte, NDArrayFloat, PathType # Mapping from egovehicle time in nanoseconds to egovehicle pose. TimestampedCitySE3EgoPoses = Dict[int, SE3] @@ -22,7 +23,9 @@ SensorPosesMapping = Dict[str, SE3] -def read_feather(path: Path, columns: Optional[Tuple[str, ...]] = None) -> pd.DataFrame: +def read_feather( + path: PathType, columns: Optional[Tuple[str, ...]] = None +) -> pd.DataFrame: """Read Apache Feather data from a .feather file. AV2 uses .feather to serialize much of its data. This function handles the deserialization @@ -36,8 +39,11 @@ def read_feather(path: Path, columns: Optional[Tuple[str, ...]] = None) -> pd.Da Returns: (N,len(columns)) Apache Feather data represented as a `pandas` DataFrame. """ - data: pd.DataFrame = feather.read_feather(path, columns=columns) - return data + with path.open("rb") as file_handle: + dataframe: pd.DataFrame = feather.read_feather( + file_handle, columns=columns, memory_map=True + ) + return dataframe def read_lidar_sweep(fpath: Path, attrib_spec: str = "xyz") -> NDArrayFloat: @@ -63,7 +69,9 @@ def read_lidar_sweep(fpath: Path, attrib_spec: str = "xyz") -> NDArrayFloat: """ possible_attributes = ["x", "y", "z"] if not all([a in possible_attributes for a in attrib_spec]): - raise ValueError("Attributes must be in (x, y, z, intensity, laser_number, offset_ns).") + raise ValueError( + "Attributes must be in (x, y, z, intensity, laser_number, offset_ns)." + ) sweep_df = read_feather(fpath) @@ -106,17 +114,20 @@ def read_ego_SE3_sensor(log_dir: Path) -> SensorPosesMapping: """ ego_SE3_sensor_path = Path(log_dir, "calibration", "egovehicle_SE3_sensor.feather") ego_SE3_sensor = read_feather(ego_SE3_sensor_path) - rotations = geometry_utils.quat_to_mat(ego_SE3_sensor.loc[:, ["qw", "qx", "qy", "qz"]].to_numpy()) + rotations = geometry_utils.quat_to_mat( + ego_SE3_sensor.loc[:, ["qw", "qx", "qy", "qz"]].to_numpy() + ) translations = ego_SE3_sensor.loc[:, ["tx_m", "ty_m", "tz_m"]].to_numpy() sensor_names = ego_SE3_sensor.loc[:, "sensor_name"].to_numpy() sensor_name_to_pose: SensorPosesMapping = { - name: SE3(rotation=rotations[i], translation=translations[i]) for i, name in enumerate(sensor_names) + name: SE3(rotation=rotations[i], translation=translations[i]) + for i, name in enumerate(sensor_names) } return sensor_name_to_pose -def read_city_SE3_ego(log_dir: Path) -> TimestampedCitySE3EgoPoses: +def read_city_SE3_ego(log_dir: Union[Path, UPath]) -> TimestampedCitySE3EgoPoses: """Read the egovehicle poses in the city reference frame. The egovehicle city pose defines an SE3 transformation from the egovehicle reference frame to the city ref. frame. @@ -148,7 +159,7 @@ def read_city_SE3_ego(log_dir: Path) -> TimestampedCitySE3EgoPoses: Returns: Mapping from egovehicle time (in nanoseconds) to egovehicle pose in the city reference frame. """ - city_SE3_ego_path = Path(log_dir, "city_SE3_egovehicle.feather") + city_SE3_ego_path = log_dir / "city_SE3_egovehicle.feather" city_SE3_ego = read_feather(city_SE3_ego_path) quat_wxyz = city_SE3_ego.loc[:, ["qw", "qx", "qy", "qz"]].to_numpy() @@ -157,7 +168,8 @@ def read_city_SE3_ego(log_dir: Path) -> TimestampedCitySE3EgoPoses: rotation = geometry_utils.quat_to_mat(quat_wxyz) timestamp_city_SE3_ego_dict: TimestampedCitySE3EgoPoses = { - ts: SE3(rotation=rotation[i], translation=translation_xyz_m[i]) for i, ts in enumerate(timestamps_ns) + ts: SE3(rotation=rotation[i], translation=translation_xyz_m[i]) + for i, ts in enumerate(timestamps_ns) } return timestamp_city_SE3_ego_dict @@ -201,12 +213,12 @@ def write_img(img_path: Path, img: NDArrayByte, channel_order: str = "RGB") -> N raise ValueError("Unsupported channel order (must be BGR or RGB).") if channel_order == "RGB": - img: NDArrayByte = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) # type: ignore + img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) cv2.imwrite(str(img_path), img) -def read_json_file(fpath: Path) -> Dict[str, Any]: +def read_json_file(fpath: Union[Path, UPath]) -> Dict[str, Any]: """Load dictionary from JSON file. Args: @@ -215,7 +227,7 @@ def read_json_file(fpath: Path) -> Dict[str, Any]: Returns: Deserialized Python dictionary. """ - with open(fpath, "rb") as f: + with fpath.open("rb") as f: data: Dict[str, Any] = json.load(f) return data @@ -250,5 +262,8 @@ def read_all_annotations(dataset_dir: Path, split: str) -> pd.DataFrame: annotations_list: List[pd.DataFrame] = [] for annotations_path in annotations_path_list: annotations = read_feather(annotations_path) + + log_id = annotations_path.parent.stem + annotations["log_id"] = log_id annotations_list.append(annotations) return pd.concat(annotations_list).reset_index(drop=True) diff --git a/src/av2/utils/metric_time.py b/src/av2/utils/metric_time.py index 911761f2..36e55d35 100644 --- a/src/av2/utils/metric_time.py +++ b/src/av2/utils/metric_time.py @@ -26,6 +26,11 @@ def to_metric_time(ts: Union[int, float], src: TimeUnit, dst: TimeUnit) -> float Returns: timestamp expressed now in `dst` units of metric time """ - units_per_sec = {TimeUnit.Second: 1, TimeUnit.Millisecond: 1e3, TimeUnit.Microsecond: 1e6, TimeUnit.Nanosecond: 1e9} + units_per_sec = { + TimeUnit.Second: 1, + TimeUnit.Millisecond: 1e3, + TimeUnit.Microsecond: 1e6, + TimeUnit.Nanosecond: 1e9, + } # ts is in units of `src`, which will cancel with the denominator return ts * (units_per_sec[dst] / units_per_sec[src]) diff --git a/src/av2/utils/raster.py b/src/av2/utils/raster.py index ebcdee77..cbe9c32a 100644 --- a/src/av2/utils/raster.py +++ b/src/av2/utils/raster.py @@ -11,7 +11,9 @@ from av2.utils.typing import NDArrayByte, NDArrayFloat -def get_mask_from_polygons(polygons: List[NDArrayFloat], img_h: int, img_w: int) -> NDArrayByte: +def get_mask_from_polygons( + polygons: List[NDArrayFloat], img_h: int, img_w: int +) -> NDArrayByte: """Rasterize multiple polygons onto a single 2d grid/array. NOTE: Pillow can gracefully handle the scenario when a polygon has coordinates outside of the grid, @@ -34,7 +36,9 @@ def get_mask_from_polygons(polygons: List[NDArrayFloat], img_h: int, img_w: int) return mask -def blend_images(img0: NDArrayByte, img1: NDArrayByte, alpha: float = 0.7) -> NDArrayByte: +def blend_images( + img0: NDArrayByte, img1: NDArrayByte, alpha: float = 0.7 +) -> NDArrayByte: """Alpha-blend two images together. Args: diff --git a/src/av2/utils/synchronization_database.py b/src/av2/utils/synchronization_database.py index 8bcd5e63..72df0579 100644 --- a/src/av2/utils/synchronization_database.py +++ b/src/av2/utils/synchronization_database.py @@ -25,16 +25,24 @@ # constants defined in milliseconds # below evaluates to 33.3 ms -RING_CAM_SHUTTER_INTERVAL_MS: Final[float] = to_metric_time(ts=1 / RING_CAM_FPS, src=Second, dst=Millisecond) +RING_CAM_SHUTTER_INTERVAL_MS: Final[float] = to_metric_time( + ts=1 / RING_CAM_FPS, src=Second, dst=Millisecond +) # below evaluates to 200 ms -STEREO_CAM_SHUTTER_INTERVAL_MS: Final[float] = to_metric_time(ts=1 / STEREO_CAM_FPS, src=Second, dst=Millisecond) +STEREO_CAM_SHUTTER_INTERVAL_MS: Final[float] = to_metric_time( + ts=1 / STEREO_CAM_FPS, src=Second, dst=Millisecond +) # below evaluates to 100 ms -LIDAR_SWEEP_INTERVAL_MS: Final[float] = to_metric_time(ts=1 / LIDAR_FRAME_RATE_HZ, src=Second, dst=Millisecond) +LIDAR_SWEEP_INTERVAL_MS: Final[float] = to_metric_time( + ts=1 / LIDAR_FRAME_RATE_HZ, src=Second, dst=Millisecond +) ALLOWED_TIMESTAMP_BUFFER_MS: Final[int] = 2 # allow 2 ms of buffer -LIDAR_SWEEP_INTERVAL_W_BUFFER_MS: Final[float] = LIDAR_SWEEP_INTERVAL_MS + ALLOWED_TIMESTAMP_BUFFER_MS +LIDAR_SWEEP_INTERVAL_W_BUFFER_MS: Final[float] = ( + LIDAR_SWEEP_INTERVAL_MS + ALLOWED_TIMESTAMP_BUFFER_MS +) def get_timestamps_from_sensor_folder(sensor_folder_wildcard: str) -> NDArrayInt: @@ -50,11 +58,15 @@ def get_timestamps_from_sensor_folder(sensor_folder_wildcard: str) -> NDArrayInt path_generator = glob.glob(sensor_folder_wildcard) path_generator.sort() - timestamps: NDArrayInt = np.array([int(Path(jpg_fpath).stem.split("_")[-1]) for jpg_fpath in path_generator]) + timestamps: NDArrayInt = np.array( + [int(Path(jpg_fpath).stem.split("_")[-1]) for jpg_fpath in path_generator] + ) return timestamps -def find_closest_integer_in_ref_arr(query_int: int, ref_arr: NDArrayInt) -> Tuple[int, int]: +def find_closest_integer_in_ref_arr( + query_int: int, ref_arr: NDArrayInt +) -> Tuple[int, int]: """Find the closest integer to any integer inside a reference array, and the corresponding difference. In our use case, the query integer represents a nanosecond-discretized timestamp, and the @@ -72,7 +84,9 @@ def find_closest_integer_in_ref_arr(query_int: int, ref_arr: NDArrayInt) -> Tupl integer, representing the integer difference between the match and query integers """ closest_ind = np.argmin(np.absolute(ref_arr - query_int)) - closest_int = cast(int, ref_arr[closest_ind]) # mypy does not understand numpy arrays + closest_int = cast( + int, ref_arr[closest_ind] + ) # mypy does not understand numpy arrays int_diff = np.absolute(query_int - closest_int) return closest_int, int_diff @@ -101,7 +115,9 @@ class SynchronizationDB: ts=LIDAR_SWEEP_INTERVAL_W_BUFFER_MS / 2, src=Millisecond, dst=Nanosecond ) - def __init__(self, dataset_dir: str, collect_single_log_id: Optional[str] = None) -> None: + def __init__( + self, dataset_dir: str, collect_single_log_id: Optional[str] = None + ) -> None: """Build the SynchronizationDB. Note that the timestamps for each camera channel are not identical, but they are clustered together. @@ -125,9 +141,15 @@ def __init__(self, dataset_dir: str, collect_single_log_id: Optional[str] = None self.per_log_cam_timestamps_index[log_id] = {} for cam_name in list(RingCameras) + list(StereoCameras): - sensor_folder_wildcard = f"{dataset_dir}/{log_id}/sensors/cameras/{cam_name}/*.jpg" - cam_timestamps = get_timestamps_from_sensor_folder(sensor_folder_wildcard) - self.per_log_cam_timestamps_index[log_id][cam_name] = cam_timestamps + sensor_folder_wildcard = ( + f"{dataset_dir}/{log_id}/sensors/cameras/{cam_name.value}/*.jpg" + ) + cam_timestamps = get_timestamps_from_sensor_folder( + sensor_folder_wildcard + ) + self.per_log_cam_timestamps_index[log_id][ + cam_name.value + ] = cam_timestamps sensor_folder_wildcard = f"{dataset_dir}/{log_id}/sensors/lidar/*.feather" lidar_timestamps = get_timestamps_from_sensor_folder(sensor_folder_wildcard) @@ -137,7 +159,9 @@ def get_valid_logs(self) -> Iterable[str]: """Return the log_ids for which the SynchronizationDatabase contains pose information.""" return self.per_log_cam_timestamps_index.keys() - def get_closest_lidar_timestamp(self, cam_timestamp_ns: int, log_id: str) -> Optional[int]: + def get_closest_lidar_timestamp( + self, cam_timestamp_ns: int, log_id: str + ) -> Optional[int]: """Given an image timestamp, find the synchronized corresponding LiDAR timestamp. This LiDAR timestamp should have the closest absolute timestamp to the image timestamp. @@ -157,18 +181,26 @@ def get_closest_lidar_timestamp(self, cam_timestamp_ns: int, log_id: str) -> Opt if not lidar_timestamps.tolist(): return None - closest_lidar_timestamp, timestamp_diff = find_closest_integer_in_ref_arr(cam_timestamp_ns, lidar_timestamps) + closest_lidar_timestamp, timestamp_diff = find_closest_integer_in_ref_arr( + cam_timestamp_ns, lidar_timestamps + ) if timestamp_diff > self.MAX_LIDAR_ANYCAM_TIMESTAMP_DIFF: # convert to nanoseconds->milliseconds for readability logger.warning( "No corresponding LiDAR sweep: %s > %s ms", to_metric_time(ts=timestamp_diff, src=Nanosecond, dst=Millisecond), - to_metric_time(ts=self.MAX_LIDAR_ANYCAM_TIMESTAMP_DIFF, src=Nanosecond, dst=Millisecond), + to_metric_time( + ts=self.MAX_LIDAR_ANYCAM_TIMESTAMP_DIFF, + src=Nanosecond, + dst=Millisecond, + ), ) return None return closest_lidar_timestamp - def get_closest_cam_channel_timestamp(self, lidar_timestamp: int, cam_name: str, log_id: str) -> Optional[int]: + def get_closest_cam_channel_timestamp( + self, lidar_timestamp: int, cam_name: str, log_id: str + ) -> Optional[int]: """Given a LiDAR timestamp, find the synchronized corresponding image timestamp for a particular camera. This image timestamp should have the closest absolute timestamp. @@ -181,7 +213,10 @@ def get_closest_cam_channel_timestamp(self, lidar_timestamp: int, cam_name: str, Returns: closest_cam_ch_timestamp: closest timestamp """ - if log_id not in self.per_log_cam_timestamps_index or cam_name not in self.per_log_cam_timestamps_index[log_id]: + if ( + log_id not in self.per_log_cam_timestamps_index + or cam_name not in self.per_log_cam_timestamps_index[log_id] + ): return None cam_timestamps = self.per_log_cam_timestamps_index[log_id][cam_name] @@ -189,23 +224,39 @@ def get_closest_cam_channel_timestamp(self, lidar_timestamp: int, cam_name: str, if not cam_timestamps.tolist(): return None - closest_cam_ch_timestamp, timestamp_diff = find_closest_integer_in_ref_arr(lidar_timestamp, cam_timestamps) - if timestamp_diff > self.MAX_LIDAR_RING_CAM_TIMESTAMP_DIFF and cam_name in list(RingCameras): + closest_cam_ch_timestamp, timestamp_diff = find_closest_integer_in_ref_arr( + lidar_timestamp, cam_timestamps + ) + if ( + timestamp_diff > self.MAX_LIDAR_RING_CAM_TIMESTAMP_DIFF + and cam_name in list(RingCameras) + ): # convert to nanoseconds->milliseconds for readability logger.warning( "No corresponding ring image at %s: %.1f > %s ms", lidar_timestamp, to_metric_time(ts=timestamp_diff, src=Nanosecond, dst=Millisecond), - to_metric_time(ts=self.MAX_LIDAR_RING_CAM_TIMESTAMP_DIFF, src=Nanosecond, dst=Millisecond), + to_metric_time( + ts=self.MAX_LIDAR_RING_CAM_TIMESTAMP_DIFF, + src=Nanosecond, + dst=Millisecond, + ), ) return None - elif timestamp_diff > self.MAX_LIDAR_STEREO_CAM_TIMESTAMP_DIFF and cam_name in list(StereoCameras): + elif ( + timestamp_diff > self.MAX_LIDAR_STEREO_CAM_TIMESTAMP_DIFF + and cam_name in list(StereoCameras) + ): # convert to nanoseconds->milliseconds for readability logger.warning( "No corresponding stereo image at %s: %.1f > %s ms", lidar_timestamp, to_metric_time(ts=timestamp_diff, src=Nanosecond, dst=Millisecond), - to_metric_time(ts=self.MAX_LIDAR_STEREO_CAM_TIMESTAMP_DIFF, src=Nanosecond, dst=Millisecond), + to_metric_time( + ts=self.MAX_LIDAR_STEREO_CAM_TIMESTAMP_DIFF, + src=Nanosecond, + dst=Millisecond, + ), ) return None return closest_cam_ch_timestamp diff --git a/src/av2/utils/typing.py b/src/av2/utils/typing.py index 096567f9..75086a7b 100644 --- a/src/av2/utils/typing.py +++ b/src/av2/utils/typing.py @@ -4,14 +4,19 @@ from __future__ import annotations -from typing import Any # noqa +from pathlib import Path +from typing import Any, Union # noqa import numpy as np import numpy.typing as npt +from upath import UPath NDArrayNumber = npt.NDArray["np.number[Any]"] NDArrayBool = npt.NDArray[np.bool_] NDArrayFloat = npt.NDArray[np.float64] +NDArrayFloat32 = npt.NDArray[np.float32] NDArrayByte = npt.NDArray[np.uint8] NDArrayInt = npt.NDArray[np.int64] NDArrayObject = npt.NDArray[np.object_] + +PathType = Union[Path, UPath] diff --git a/integration_tests/verify_tbv_download.py b/tests/integration/verify_tbv_download.py similarity index 84% rename from integration_tests/verify_tbv_download.py rename to tests/integration/verify_tbv_download.py index cd9498f1..d1f2bc07 100644 --- a/integration_tests/verify_tbv_download.py +++ b/tests/integration/verify_tbv_download.py @@ -5,7 +5,7 @@ import logging import sys from pathlib import Path -from typing import Final, List, Tuple +from typing import Final, Tuple import click from rich.progress import track @@ -67,7 +67,9 @@ def verify_log_contents(data_root: Path, log_id: str, check_image_sizes: bool) - if not check_image_sizes: continue # every image should be (H,W) = 2048x1550 (front-center) or 775x1024 for all other cameras. - for img_fpath in track(img_fpaths, description=f"Verifying image sizes for {camera_enum}"): + for img_fpath in track( + img_fpaths, description=f"Verifying image sizes for {camera_enum}" + ): img = io_utils.read_img(img_path=img_fpath, channel_order="RGB") if camera_enum == RingCameras.RING_FRONT_CENTER: assert img.shape == (2048, 1550, 3) @@ -84,14 +86,34 @@ def verify_log_contents(data_root: Path, log_id: str, check_image_sizes: bool) - assert poses_fpath.exists() # poses file should be loadable. poses_df = io_utils.read_feather(poses_fpath) - assert list(poses_df.keys()) == ["timestamp_ns", "qw", "qx", "qy", "qz", "tx_m", "ty_m", "tz_m"] + assert list(poses_df.keys()) == [ + "timestamp_ns", + "qw", + "qx", + "qy", + "qz", + "tx_m", + "ty_m", + "tz_m", + ] # every log should have an extrinsics calibration file. - extrinsics_fpath = data_root / log_id / "calibration" / "egovehicle_SE3_sensor.feather" + extrinsics_fpath = ( + data_root / log_id / "calibration" / "egovehicle_SE3_sensor.feather" + ) assert extrinsics_fpath.exists() # extrinsics should be loadable. extrinsics_df = io_utils.read_feather(extrinsics_fpath) - assert list(extrinsics_df.keys()) == ["sensor_name", "qw", "qx", "qy", "qz", "tx_m", "ty_m", "tz_m"] + assert list(extrinsics_df.keys()) == [ + "sensor_name", + "qw", + "qx", + "qy", + "qz", + "tx_m", + "ty_m", + "tz_m", + ] # extrinsics should be provided for each camera. for camera_enum in list(RingCameras): @@ -138,9 +160,10 @@ def verify_log_map(data_root: Path, log_id: str) -> None: assert log_map_dirpath.exists() # every log should have one and only one raster height map. (Note: season is stripped from uuid here). - ground_height_raster_fpaths = list(log_map_dirpath.glob("*_ground_height_surface____*.npy")) + ground_height_raster_fpaths = list( + log_map_dirpath.glob("*_ground_height_surface____*.npy") + ) assert len(ground_height_raster_fpaths) == 1 - ground_height_raster_fpath = ground_height_raster_fpaths[0] # every log should have a Sim(2) mapping from raster grid coordinates to city coordinates. Sim2_fpaths = list(log_map_dirpath.glob("*___img_Sim2_city.json")) @@ -156,7 +179,11 @@ def verify_log_map(data_root: Path, log_id: str) -> None: # every vector map file should have only 3 keys -- "pedestrian_crossings", "lane_segments", "drivable_areas" vector_map_json_data = io_utils.read_json_file(vector_map_fpath) - assert list(vector_map_json_data.keys()) == ["pedestrian_crossings", "lane_segments", "drivable_areas"] + assert list(vector_map_json_data.keys()) == [ + "pedestrian_crossings", + "lane_segments", + "drivable_areas", + ] for _, lane_segment_dict in vector_map_json_data["lane_segments"].items(): assert tuple(lane_segment_dict.keys()) == EXPECTED_LANE_SEGMENT_ATTRIB_KEYS @@ -165,10 +192,14 @@ def verify_log_map(data_root: Path, log_id: str) -> None: avm = ArgoverseStaticMap.from_json(static_map_path=vector_map_fpath) # every map should be loadable w/ build_raster=False - avm = ArgoverseStaticMap.from_map_dir(log_map_dirpath=log_map_dirpath, build_raster=False) + avm = ArgoverseStaticMap.from_map_dir( + log_map_dirpath=log_map_dirpath, build_raster=False + ) # every map should be loadable w/ build_raster=True. - avm = ArgoverseStaticMap.from_map_dir(log_map_dirpath=log_map_dirpath, build_raster=True) + avm = ArgoverseStaticMap.from_map_dir( + log_map_dirpath=log_map_dirpath, build_raster=True + ) # load every lane segment lane_segments = avm.get_scenario_lane_segments() @@ -181,13 +212,13 @@ def verify_log_map(data_root: Path, log_id: str) -> None: assert left_lane_boundary.ndim == 2 and left_lane_boundary.shape[1] == 3 # load every pedestrian crossing - pcs = avm.get_scenario_ped_crossings() + avm.get_scenario_ped_crossings() # load every drivable area - das = avm.get_scenario_vector_drivable_areas() + avm.get_scenario_vector_drivable_areas() -def verify_logs_using_dataloader(data_root: Path, log_ids: List[str]) -> None: +def verify_logs_using_dataloader(data_root: Path, log_ids: Tuple[str, ...]) -> None: """Use a dataloader object to query each log's data, and verify it. Args: @@ -195,8 +226,9 @@ def verify_logs_using_dataloader(data_root: Path, log_ids: List[str]) -> None: log_ids: unique IDs of TbV vehicle logs. """ loader = AV2SensorDataLoader(data_dir=data_root, labels_dir=data_root) - for log_id in track(log_ids, description="Verify logs using an AV2 dataloader object"): - + for log_id in track( + log_ids, description="Verify logs using an AV2 dataloader object" + ): logger.info("Verifying log %s", log_id) # city abbreviation should be parsable from every vector map file name, and should fall into 1 of 6 cities city_name = loader.get_city_name(log_id=log_id) @@ -205,7 +237,9 @@ def verify_logs_using_dataloader(data_root: Path, log_ids: List[str]) -> None: # pose should be present for every lidar sweep. lidar_timestamps_ns = loader.get_ordered_log_lidar_timestamps(log_id=log_id) for lidar_timestamp_ns in lidar_timestamps_ns: - city_SE3_egovehicle = loader.get_city_SE3_ego(log_id=log_id, timestamp_ns=lidar_timestamp_ns) + city_SE3_egovehicle = loader.get_city_SE3_ego( + log_id=log_id, timestamp_ns=lidar_timestamp_ns + ) assert isinstance(city_SE3_egovehicle, SE3) @@ -232,7 +266,11 @@ def run_verify_all_tbv_logs(data_root: str, check_image_sizes: bool) -> None: for i in range(num_logs): log_id = log_ids[i] logger.info("Verifying log %d: %s", i, log_id) - verify_log_contents(data_root=Path(data_root), log_id=log_id, check_image_sizes=check_image_sizes) + verify_log_contents( + data_root=Path(data_root), + log_id=log_id, + check_image_sizes=check_image_sizes, + ) verify_logs_using_dataloader(data_root=Path(data_root), log_ids=log_ids) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..46816ddf --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ +"""Tests package.""" diff --git a/tests/conftest.py b/tests/unit/conftest.py similarity index 100% rename from tests/conftest.py rename to tests/unit/conftest.py diff --git a/tests/datasets/motion_forecasting/eval/test_metrics.py b/tests/unit/datasets/motion_forecasting/eval/test_metrics.py similarity index 74% rename from tests/datasets/motion_forecasting/eval/test_metrics.py rename to tests/unit/datasets/motion_forecasting/eval/test_metrics.py index 4ffd4afa..9ba1a7f1 100644 --- a/tests/datasets/motion_forecasting/eval/test_metrics.py +++ b/tests/unit/datasets/motion_forecasting/eval/test_metrics.py @@ -10,7 +10,7 @@ import pytest import av2.datasets.motion_forecasting.eval.metrics as metrics -from av2.utils.typing import NDArrayFloat, NDArrayNumber +from av2.utils.typing import NDArrayFloat # Build stationary GT trajectory at (0, 0) test_N: Final[int] = 10 @@ -27,19 +27,27 @@ expected_fde_stationary_k6 = np.full((6,), np.sqrt(2)) # Case 3: K=1 forecast in straight line on X axis -forecasted_trajectories_straight_k1 = np.stack([np.arange(test_N), np.zeros(test_N)], axis=1)[np.newaxis, ...] # 1xNx2 +forecasted_trajectories_straight_k1 = np.stack( + [np.arange(test_N), np.zeros(test_N)], axis=1 +)[ + np.newaxis, ... +] # 1xNx2 expected_ade_straight_k1 = np.full((1,), np.arange(test_N).mean()) expected_fde_straight_k1 = np.full((1,), test_N - 1) # Case 4: K=6 forecasts in straight line on X axis -forecasted_trajectories_straight_k6: NDArrayFloat = np.concatenate( # type: ignore +forecasted_trajectories_straight_k6: NDArrayFloat = np.concatenate( [forecasted_trajectories_straight_k1] * 6, axis=0 ) # 6xNx2 expected_ade_straight_k6 = np.full((6,), np.arange(test_N).mean()) expected_fde_straight_k6 = np.full((6,), test_N - 1) # Case 5: K=1 forecast in diagonal line -forecasted_trajectories_diagonal_k1 = np.stack([np.arange(test_N), np.arange(test_N)], axis=1)[np.newaxis, ...] # 1xNx2 +forecasted_trajectories_diagonal_k1 = np.stack( + [np.arange(test_N), np.arange(test_N)], axis=1 +)[ + np.newaxis, ... +] # 1xNx2 expected_ade_diagonal_k1 = np.full((1,), 6.36396103) expected_fde_diagonal_k1 = np.full((1,), np.hypot(test_N - 1, test_N - 1)) @@ -55,7 +63,9 @@ ], ids=["stationary_k1", "stationary_k6", "straight_k1", "straight_k6", "diagonal_k1"], ) -def test_compute_ade(forecasted_trajectories: NDArrayNumber, expected_ade: NDArrayFloat) -> None: +def test_compute_ade( + forecasted_trajectories: NDArrayFloat, expected_ade: NDArrayFloat +) -> None: """Test that compute_ade returns the correct output with valid inputs. Args: @@ -63,7 +73,7 @@ def test_compute_ade(forecasted_trajectories: NDArrayNumber, expected_ade: NDArr expected_ade: Expected average displacement error. """ ade = metrics.compute_ade(forecasted_trajectories, _STATIONARY_GT_TRAJ) - np.testing.assert_allclose(ade, expected_ade) # type: ignore + np.testing.assert_allclose(ade, expected_ade) @pytest.mark.parametrize( @@ -77,7 +87,9 @@ def test_compute_ade(forecasted_trajectories: NDArrayNumber, expected_ade: NDArr ], ids=["stationary_k1", "stationary_k6", "straight_k1", "straight_k6", "diagonal_k1"], ) -def test_compute_fde(forecasted_trajectories: NDArrayNumber, expected_fde: NDArrayFloat) -> None: +def test_compute_fde( + forecasted_trajectories: NDArrayFloat, expected_fde: NDArrayFloat +) -> None: """Test that compute_fde returns the correct output with valid inputs. Args: @@ -93,14 +105,26 @@ def test_compute_fde(forecasted_trajectories: NDArrayNumber, expected_fde: NDArr [ (forecasted_trajectories_stationary_k1, 2.0, False), (forecasted_trajectories_stationary_k6, 2.0, False), - (forecasted_trajectories_straight_k1, expected_fde_straight_k1[0] + 0.01, False), + ( + forecasted_trajectories_straight_k1, + expected_fde_straight_k1[0] + 0.01, + False, + ), (forecasted_trajectories_straight_k1, expected_fde_straight_k1[0] - 0.01, True), (forecasted_trajectories_diagonal_k1, 2.0, True), ], - ids=["stationary_k1", "stationary_k6", "straight_below_threshold", "straight_above_threshold", "diagonal"], + ids=[ + "stationary_k1", + "stationary_k6", + "straight_below_threshold", + "straight_above_threshold", + "diagonal", + ], ) def test_compute_is_missed_prediction( - forecasted_trajectories: NDArrayNumber, miss_threshold_m: float, expected_is_missed_label: bool + forecasted_trajectories: NDArrayFloat, + miss_threshold_m: float, + expected_is_missed_label: bool, ) -> None: """Test that compute_is_missed_prediction returns the correct output with valid inputs. @@ -126,21 +150,49 @@ def test_compute_is_missed_prediction( wrong_shape_probabilities_k6: NDArrayFloat = np.ones((5,)) / 5 expected_bade_uniform_k1 = expected_ade_stationary_k1 -expected_bade_uniform_k6 = expected_ade_straight_k6 + np.square((1 - uniform_probabilities_k6)) -expected_bade_confident_k6 = expected_ade_straight_k6 + np.square((1 - confident_probabilities_k6)) +expected_bade_uniform_k6 = expected_ade_straight_k6 + np.square( + (1 - uniform_probabilities_k6) +) +expected_bade_confident_k6 = expected_ade_straight_k6 + np.square( + (1 - confident_probabilities_k6) +) expected_bfde_uniform_k1 = expected_fde_stationary_k1 -expected_bfde_uniform_k6 = expected_fde_straight_k6 + np.square((1 - uniform_probabilities_k6)) -expected_bfde_confident_k6 = expected_fde_straight_k6 + np.square((1 - confident_probabilities_k6)) +expected_bfde_uniform_k6 = expected_fde_straight_k6 + np.square( + (1 - uniform_probabilities_k6) +) +expected_bfde_confident_k6 = expected_fde_straight_k6 + np.square( + (1 - confident_probabilities_k6) +) @pytest.mark.parametrize( "forecasted_trajectories, forecast_probabilities, normalize, expected_brier_ade", [ - (forecasted_trajectories_stationary_k1, uniform_probabilities_k1, False, expected_bade_uniform_k1), - (forecasted_trajectories_straight_k6, uniform_probabilities_k6, False, expected_bade_uniform_k6), - (forecasted_trajectories_straight_k6, confident_probabilities_k6, False, expected_bade_confident_k6), - (forecasted_trajectories_straight_k6, non_normalized_probabilities_k6, True, expected_bade_confident_k6), + ( + forecasted_trajectories_stationary_k1, + uniform_probabilities_k1, + False, + expected_bade_uniform_k1, + ), + ( + forecasted_trajectories_straight_k6, + uniform_probabilities_k6, + False, + expected_bade_uniform_k6, + ), + ( + forecasted_trajectories_straight_k6, + confident_probabilities_k6, + False, + expected_bade_confident_k6, + ), + ( + forecasted_trajectories_straight_k6, + non_normalized_probabilities_k6, + True, + expected_bade_confident_k6, + ), ], ids=[ "uniform_stationary_k1", @@ -150,8 +202,8 @@ def test_compute_is_missed_prediction( ], ) def test_compute_brier_ade( - forecasted_trajectories: NDArrayNumber, - forecast_probabilities: NDArrayNumber, + forecasted_trajectories: NDArrayFloat, + forecast_probabilities: NDArrayFloat, normalize: bool, expected_brier_ade: NDArrayFloat, ) -> None: @@ -183,7 +235,7 @@ def test_compute_brier_ade( ], ) def test_compute_brier_ade_data_validation( - forecast_probabilities: NDArrayNumber, normalize: bool, expectation: AbstractContextManager # type: ignore + forecast_probabilities: NDArrayFloat, normalize: bool, expectation: AbstractContextManager # type: ignore ) -> None: """Test that test_compute_brier_ade raises the correct errors when inputs are invalid. @@ -194,17 +246,40 @@ def test_compute_brier_ade_data_validation( """ with expectation: metrics.compute_brier_ade( - forecasted_trajectories_straight_k6, _STATIONARY_GT_TRAJ, forecast_probabilities, normalize + forecasted_trajectories_straight_k6, + _STATIONARY_GT_TRAJ, + forecast_probabilities, + normalize, ) @pytest.mark.parametrize( "forecasted_trajectories, forecast_probabilities, normalize, expected_brier_fde", [ - (forecasted_trajectories_stationary_k1, uniform_probabilities_k1, False, expected_bfde_uniform_k1), - (forecasted_trajectories_straight_k6, uniform_probabilities_k6, False, expected_bfde_uniform_k6), - (forecasted_trajectories_straight_k6, confident_probabilities_k6, False, expected_bfde_confident_k6), - (forecasted_trajectories_straight_k6, non_normalized_probabilities_k6, True, expected_bfde_confident_k6), + ( + forecasted_trajectories_stationary_k1, + uniform_probabilities_k1, + False, + expected_bfde_uniform_k1, + ), + ( + forecasted_trajectories_straight_k6, + uniform_probabilities_k6, + False, + expected_bfde_uniform_k6, + ), + ( + forecasted_trajectories_straight_k6, + confident_probabilities_k6, + False, + expected_bfde_confident_k6, + ), + ( + forecasted_trajectories_straight_k6, + non_normalized_probabilities_k6, + True, + expected_bfde_confident_k6, + ), ], ids=[ "uniform_stationary_k1", @@ -214,8 +289,8 @@ def test_compute_brier_ade_data_validation( ], ) def test_compute_brier_fde( - forecasted_trajectories: NDArrayNumber, - forecast_probabilities: NDArrayNumber, + forecasted_trajectories: NDArrayFloat, + forecast_probabilities: NDArrayFloat, normalize: bool, expected_brier_fde: NDArrayFloat, ) -> None: @@ -247,7 +322,7 @@ def test_compute_brier_fde( ], ) def test_compute_brier_fde_data_validation( - forecast_probabilities: NDArrayNumber, normalize: bool, expectation: AbstractContextManager # type: ignore + forecast_probabilities: NDArrayFloat, normalize: bool, expectation: AbstractContextManager # type: ignore ) -> None: """Test that test_compute_brier_fde raises the correct errors when inputs are invalid. @@ -258,5 +333,8 @@ def test_compute_brier_fde_data_validation( """ with expectation: metrics.compute_brier_fde( - forecasted_trajectories_straight_k6, _STATIONARY_GT_TRAJ, forecast_probabilities, normalize + forecasted_trajectories_straight_k6, + _STATIONARY_GT_TRAJ, + forecast_probabilities, + normalize, ) diff --git a/tests/datasets/motion_forecasting/eval/test_submission.py b/tests/unit/datasets/motion_forecasting/eval/test_submission.py similarity index 56% rename from tests/datasets/motion_forecasting/eval/test_submission.py rename to tests/unit/datasets/motion_forecasting/eval/test_submission.py index a60a88bd..091298c0 100644 --- a/tests/datasets/motion_forecasting/eval/test_submission.py +++ b/tests/unit/datasets/motion_forecasting/eval/test_submission.py @@ -11,22 +11,51 @@ import pytest from av2.datasets.motion_forecasting.constants import AV2_SCENARIO_PRED_TIMESTEPS -from av2.datasets.motion_forecasting.eval.submission import ChallengeSubmission, ScenarioPredictions, TrackPredictions +from av2.datasets.motion_forecasting.eval.submission import ( + ChallengeSubmission, + ScenarioPredictions, + ScenarioProbabilities, + ScenarioTrajectories, + TrackTrajectories, +) # Build valid submission with predictions for a single track in a single scenario -valid_track_predictions: TrackPredictions = (np.zeros((2, AV2_SCENARIO_PRED_TIMESTEPS, 2)), np.array([0.6, 0.4])) -valid_scenario_predictions: ScenarioPredictions = {"valid_track_id": valid_track_predictions} -valid_submission_predictions = {"valid_scenario_id": valid_scenario_predictions} +valid_track_trajectories: TrackTrajectories = np.zeros( + (2, AV2_SCENARIO_PRED_TIMESTEPS, 2) +) +valid_scenario_probabilities: ScenarioProbabilities = np.array([0.6, 0.4]) +valid_scenario_trajectories: ScenarioTrajectories = { + "valid_track_id": valid_track_trajectories +} +valid_submission_predictions = { + "valid_scenario_id": (valid_scenario_probabilities, valid_scenario_trajectories) +} # Build invalid track submission with incorrect prediction length -too_short_track_predictions: TrackPredictions = (np.zeros((1, AV2_SCENARIO_PRED_TIMESTEPS - 1, 2)), np.array([1.0])) -too_short_scenario_predictions = {"invalid_track_id": too_short_track_predictions} -too_short_submission_predictions = {"invaild_scenario_id": too_short_scenario_predictions} +too_short_track_trajectories: TrackTrajectories = np.zeros( + (1, AV2_SCENARIO_PRED_TIMESTEPS - 1, 2) +) +too_short_scenario_probabilities = np.array([1.0]) +too_short_scenario_trajectories = {"invalid_track_id": too_short_track_trajectories} +too_short_submission_predictions = { + "invalid_scenario_id": ( + too_short_scenario_probabilities, + too_short_scenario_trajectories, + ) +} # Build invalid track submission with mismatched predicted trajectories and probabilities -mismatched_track_predictions: TrackPredictions = (np.zeros((1, AV2_SCENARIO_PRED_TIMESTEPS, 2)), np.array([0.5, 0.5])) -mismatched_scenario_predictions = {"invalid_track_id": mismatched_track_predictions} -mismatched_submission_predictions = {"invaild_scenario_id": mismatched_scenario_predictions} +mismatched_track_trajectories: TrackTrajectories = np.zeros( + (1, AV2_SCENARIO_PRED_TIMESTEPS, 2) +) +mismatched_scenario_probabilities = np.array([0.5, 0.5]) +mismatched_scenario_trajectories = {"invalid_track_id": mismatched_track_trajectories} +mismatched_submission_predictions = { + "invalid_scenario_id": ( + mismatched_scenario_probabilities, + mismatched_scenario_trajectories, + ) +} @pytest.mark.parametrize( @@ -56,7 +85,9 @@ def test_challenge_submission_data_validation( [(valid_submission_predictions)], ids=["valid_submission"], ) -def test_challenge_submission_serialization(tmpdir: Path, test_submission_dict: Dict[str, ScenarioPredictions]) -> None: +def test_challenge_submission_serialization( + tmpdir: Path, test_submission_dict: Dict[str, ScenarioPredictions] +) -> None: """Test that challenge submissions can be serialized/deserialized without changes to internal state. Args: @@ -70,9 +101,16 @@ def test_challenge_submission_serialization(tmpdir: Path, test_submission_dict: deserialized_submission = ChallengeSubmission.from_parquet(submission_file_path) # Check that deserialized data matches original data exactly - for scenario_id, scenario_predictions in submission.predictions.items(): - for track_id, (expected_trajectories, expected_probabilities) in scenario_predictions.items(): - deserialized_predictions = deserialized_submission.predictions[scenario_id][track_id] - (deserialized_trajectories, deserialized_probabilities) = deserialized_predictions + for scenario_id, ( + expected_probabilities, + scenario_trajectories, + ) in submission.predictions.items(): + for track_id, expected_trajectories in scenario_trajectories.items(): + deserialized_probabilities = deserialized_submission.predictions[ + scenario_id + ][0] + deserialized_trajectories = deserialized_submission.predictions[ + scenario_id + ][1][track_id] assert np.array_equal(deserialized_trajectories, expected_trajectories) assert np.array_equal(deserialized_probabilities, expected_probabilities) diff --git a/tests/datasets/motion_forecasting/test_scenario_serialization.py b/tests/unit/datasets/motion_forecasting/test_scenario_serialization.py similarity index 69% rename from tests/datasets/motion_forecasting/test_scenario_serialization.py rename to tests/unit/datasets/motion_forecasting/test_scenario_serialization.py index 6da8cd37..c4be106e 100644 --- a/tests/datasets/motion_forecasting/test_scenario_serialization.py +++ b/tests/unit/datasets/motion_forecasting/test_scenario_serialization.py @@ -8,13 +8,25 @@ import numpy as np from av2.datasets.motion_forecasting import scenario_serialization -from av2.datasets.motion_forecasting.data_schema import ArgoverseScenario, ObjectState, ObjectType, Track, TrackCategory +from av2.datasets.motion_forecasting.data_schema import ( + ArgoverseScenario, + ObjectState, + ObjectType, + Track, + TrackCategory, +) # Build test ArgoverseScenario _TEST_OBJECT_STATES: List[ObjectState] = [ - ObjectState(observed=True, timestep=0, position=(0.0, 0.0), heading=0.0, velocity=(0.0, 0.0)), - ObjectState(observed=True, timestep=1, position=(1.0, 1.0), heading=0.0, velocity=(1.0, 1.0)), - ObjectState(observed=True, timestep=2, position=(2.0, 2.0), heading=0.0, velocity=(2.0, 2.0)), + ObjectState( + observed=True, timestep=0, position=(0.0, 0.0), heading=0.0, velocity=(0.0, 0.0) + ), + ObjectState( + observed=True, timestep=1, position=(1.0, 1.0), heading=0.0, velocity=(1.0, 1.0) + ), + ObjectState( + observed=True, timestep=2, position=(2.0, 2.0), heading=0.0, velocity=(2.0, 2.0) + ), ] _TEST_TRACKS: List[Track] = [ Track( @@ -49,20 +61,31 @@ def test_parquet_scenario_serialization_roundtrip(tmpdir: Path) -> None: """ # Serialize Argoverse scenario to parquet and save to disk scenario_path = tmpdir / "test.parquet" - scenario_serialization.serialize_argoverse_scenario_parquet(scenario_path, _TEST_SCENARIO) + scenario_serialization.serialize_argoverse_scenario_parquet( + scenario_path, _TEST_SCENARIO + ) assert scenario_path.exists(), "Serialized Argoverse scenario not saved to disk." # Check that loading and deserializing a parquet-formatted Argoverse scenario returns an equivalent object - loaded_test_scenario = scenario_serialization.load_argoverse_scenario_parquet(scenario_path) - assert loaded_test_scenario == _TEST_SCENARIO, "Deserialized Argoverse scenario did not match original object." + loaded_test_scenario = scenario_serialization.load_argoverse_scenario_parquet( + scenario_path + ) + assert ( + loaded_test_scenario == _TEST_SCENARIO + ), "Deserialized Argoverse scenario did not match original object." def test_load_argoverse_scenario_parquet(test_data_root_dir: Path) -> None: """Try to load a real scenario from the motion forecasting dataset.""" test_scenario_id = "0a1e6f0a-1817-4a98-b02e-db8c9327d151" test_scenario_path = ( - test_data_root_dir / "forecasting_scenarios" / test_scenario_id / f"scenario_{test_scenario_id}.parquet" + test_data_root_dir + / "forecasting_scenarios" + / test_scenario_id + / f"scenario_{test_scenario_id}.parquet" ) - test_scenario = scenario_serialization.load_argoverse_scenario_parquet(test_scenario_path) + test_scenario = scenario_serialization.load_argoverse_scenario_parquet( + test_scenario_path + ) assert test_scenario.scenario_id == test_scenario_id diff --git a/tests/datasets/sensor/test_av2_sensor_dataloader.py b/tests/unit/datasets/sensor/test_av2_sensor_dataloader.py similarity index 94% rename from tests/datasets/sensor/test_av2_sensor_dataloader.py rename to tests/unit/datasets/sensor/test_av2_sensor_dataloader.py index 5085f30a..b061c017 100644 --- a/tests/datasets/sensor/test_av2_sensor_dataloader.py +++ b/tests/unit/datasets/sensor/test_av2_sensor_dataloader.py @@ -47,7 +47,9 @@ def test_get_city_SE3_ego(test_data_root_dir: Path) -> None: dataroot = test_data_root_dir / "sensor_dataset_logs" loader = AV2SensorDataLoader(data_dir=dataroot, labels_dir=dataroot) - city_SE3_egovehicle = loader.get_city_SE3_ego(log_id=log_id, timestamp_ns=timestamp_ns) + city_SE3_egovehicle = loader.get_city_SE3_ego( + log_id=log_id, timestamp_ns=timestamp_ns + ) assert isinstance(city_SE3_egovehicle, SE3) expected_translation: NDArrayFloat = np.array([1468.87, 211.51, 13.14]) diff --git a/tests/datasets/sensor/test_sensor_dataloader.py b/tests/unit/datasets/sensor/test_sensor_dataloader.py similarity index 74% rename from tests/datasets/sensor/test_sensor_dataloader.py rename to tests/unit/datasets/sensor/test_sensor_dataloader.py index da83c508..fd79d73d 100644 --- a/tests/datasets/sensor/test_sensor_dataloader.py +++ b/tests/unit/datasets/sensor/test_sensor_dataloader.py @@ -24,20 +24,33 @@ def _create_dummy_sensor_dataloader(log_id: str) -> SensorDataloader: """Create a dummy sensor dataloader.""" - with Path(tempfile.TemporaryDirectory().name) as sensor_dataset_dir: - for sensor_name, timestamps_ms in SENSOR_TIMESTAMPS_MS_DICT.items(): - for t in timestamps_ms: - if "ring" in sensor_name: - fpath = Path( - sensor_dataset_dir, "dummy", log_id, "sensors", "cameras", sensor_name, f"{int(t*1e6)}.jpg" - ) - Path(fpath).parent.mkdir(exist_ok=True, parents=True) - fpath.open("w").close() - elif "lidar" in sensor_name: - fpath = Path(sensor_dataset_dir, "dummy", log_id, "sensors", sensor_name, f"{int(t*1e6)}.feather") - Path(fpath).parent.mkdir(exist_ok=True, parents=True) - fpath.open("w").close() - return SensorDataloader(dataset_dir=sensor_dataset_dir, with_cache=False) + sensor_dataset_dir = Path(tempfile.TemporaryDirectory().name) + for sensor_name, timestamps_ms in SENSOR_TIMESTAMPS_MS_DICT.items(): + for t in timestamps_ms: + if "ring" in sensor_name: + fpath = Path( + sensor_dataset_dir, + "dummy", + log_id, + "sensors", + "cameras", + sensor_name, + f"{int(t*1e6)}.jpg", + ) + Path(fpath).parent.mkdir(exist_ok=True, parents=True) + fpath.open("w").close() + elif "lidar" in sensor_name: + fpath = Path( + sensor_dataset_dir, + "dummy", + log_id, + "sensors", + sensor_name, + f"{int(t*1e6)}.feather", + ) + Path(fpath).parent.mkdir(exist_ok=True, parents=True) + fpath.open("w").close() + return SensorDataloader(dataset_dir=sensor_dataset_dir, with_cache=False) def test_sensor_data_loader_milliseconds() -> None: @@ -121,7 +134,9 @@ def test_sensor_data_loader_milliseconds() -> None: # use the non-pandas implementation as a "brute-force" (BF) check. # read out the dataset root from the other dataloader's attributes. - bf_loader = AV2SensorDataLoader(data_dir=loader.dataset_dir / "dummy", labels_dir=loader.dataset_dir / "dummy") + bf_loader = AV2SensorDataLoader( + data_dir=loader.dataset_dir / "dummy", labels_dir=loader.dataset_dir / "dummy" + ) # for every image, make sure query result matches the brute-force query result. for ring_camera_enum in RingCameras: @@ -129,9 +144,14 @@ def test_sensor_data_loader_milliseconds() -> None: for cam_timestamp_ms in SENSOR_TIMESTAMPS_MS_DICT[ring_camera_name]: cam_timestamp_ns = int(cam_timestamp_ms * 1e6) result = loader.get_closest_lidar_fpath( - split="dummy", log_id=log_id, cam_name=ring_camera_name, cam_timestamp_ns=cam_timestamp_ns + split="dummy", + log_id=log_id, + cam_name=ring_camera_name, + cam_timestamp_ns=cam_timestamp_ns, + ) + bf_result = bf_loader.get_closest_lidar_fpath( + log_id=log_id, cam_timestamp_ns=cam_timestamp_ns ) - bf_result = bf_loader.get_closest_lidar_fpath(log_id=log_id, cam_timestamp_ns=cam_timestamp_ns) assert result == bf_result # for every lidar sweep, make sure query result matches the brute-force query result. @@ -140,10 +160,15 @@ def test_sensor_data_loader_milliseconds() -> None: for ring_camera_enum in list(RingCameras): ring_camera_name = ring_camera_enum.value result = loader.get_closest_img_fpath( - split="dummy", log_id=log_id, cam_name=ring_camera_name, lidar_timestamp_ns=lidar_timestamp_ns + split="dummy", + log_id=log_id, + cam_name=ring_camera_name, + lidar_timestamp_ns=lidar_timestamp_ns, ) bf_result = bf_loader.get_closest_img_fpath( - log_id=log_id, cam_name=ring_camera_name, lidar_timestamp_ns=lidar_timestamp_ns + log_id=log_id, + cam_name=ring_camera_name, + lidar_timestamp_ns=lidar_timestamp_ns, ) assert result == bf_result diff --git a/tests/unit/evaluation/3d_tracking/test_eval.py b/tests/unit/evaluation/3d_tracking/test_eval.py new file mode 100644 index 00000000..e41415ac --- /dev/null +++ b/tests/unit/evaluation/3d_tracking/test_eval.py @@ -0,0 +1,6 @@ +"""3D Tracking evaluation unit tests.""" + + +def test_evaluate() -> None: + """Test 3D tracking evaluation.""" + pass diff --git a/tests/unit/evaluation/__init__.py b/tests/unit/evaluation/__init__.py new file mode 100644 index 00000000..701919e1 --- /dev/null +++ b/tests/unit/evaluation/__init__.py @@ -0,0 +1 @@ +"""Unit testing sub-package.""" diff --git a/tests/unit/evaluation/detection/__init__.py b/tests/unit/evaluation/detection/__init__.py new file mode 100644 index 00000000..b5f35ee9 --- /dev/null +++ b/tests/unit/evaluation/detection/__init__.py @@ -0,0 +1 @@ +"""3D Detection sub-package.""" diff --git a/tests/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/annotations.feather b/tests/unit/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/annotations.feather similarity index 100% rename from tests/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/annotations.feather rename to tests/unit/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/annotations.feather diff --git a/tests/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/city_SE3_egovehicle.feather b/tests/unit/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/city_SE3_egovehicle.feather similarity index 100% rename from tests/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/city_SE3_egovehicle.feather rename to tests/unit/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/city_SE3_egovehicle.feather diff --git a/tests/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76___img_Sim2_city.json b/tests/unit/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76___img_Sim2_city.json similarity index 100% rename from tests/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76___img_Sim2_city.json rename to tests/unit/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76___img_Sim2_city.json diff --git a/tests/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76_ground_height_surface____PIT.npy b/tests/unit/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76_ground_height_surface____PIT.npy similarity index 100% rename from tests/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76_ground_height_surface____PIT.npy rename to tests/unit/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76_ground_height_surface____PIT.npy diff --git a/tests/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/log_map_archive_adcf7d18-0510-35b0-a2fa-b4cea13a6d76____PIT_city_57819.json b/tests/unit/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/log_map_archive_adcf7d18-0510-35b0-a2fa-b4cea13a6d76____PIT_city_57819.json similarity index 100% rename from tests/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/log_map_archive_adcf7d18-0510-35b0-a2fa-b4cea13a6d76____PIT_city_57819.json rename to tests/unit/evaluation/detection/data/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/log_map_archive_adcf7d18-0510-35b0-a2fa-b4cea13a6d76____PIT_city_57819.json diff --git a/tests/evaluation/detection/data/detections.feather b/tests/unit/evaluation/detection/data/detections.feather similarity index 100% rename from tests/evaluation/detection/data/detections.feather rename to tests/unit/evaluation/detection/data/detections.feather diff --git a/tests/evaluation/detection/data/detections_assignment.feather b/tests/unit/evaluation/detection/data/detections_assignment.feather similarity index 100% rename from tests/evaluation/detection/data/detections_assignment.feather rename to tests/unit/evaluation/detection/data/detections_assignment.feather diff --git a/tests/evaluation/detection/data/detections_identity.feather b/tests/unit/evaluation/detection/data/detections_identity.feather similarity index 100% rename from tests/evaluation/detection/data/detections_identity.feather rename to tests/unit/evaluation/detection/data/detections_identity.feather diff --git a/tests/evaluation/detection/data/labels.feather b/tests/unit/evaluation/detection/data/labels.feather similarity index 100% rename from tests/evaluation/detection/data/labels.feather rename to tests/unit/evaluation/detection/data/labels.feather diff --git a/tests/evaluation/detection/test_eval.py b/tests/unit/evaluation/detection/test_eval.py similarity index 67% rename from tests/evaluation/detection/test_eval.py rename to tests/unit/evaluation/detection/test_eval.py index 58c9ece8..bbf001c8 100644 --- a/tests/evaluation/detection/test_eval.py +++ b/tests/unit/evaluation/detection/test_eval.py @@ -37,17 +37,32 @@ TEST_DATA_DIR: Final[Path] = Path(__file__).parent.resolve() / "data" -TRANSLATION_COLS: Final[List[str]] = ["tx_m", "ty_m", "tz_m"] -DIMS_COLS: Final[List[str]] = ["length_m", "width_m", "height_m"] -QUAT_COLS: Final[List[str]] = ["qw", "qx", "qy", "qz"] -ANNO_COLS: Final[List[str]] = ["timestamp_ns", "category"] + DIMS_COLS + QUAT_COLS + TRANSLATION_COLS +TRANSLATION_COLS: Final = ["tx_m", "ty_m", "tz_m"] +DIMS_COLS: Final = ["length_m", "width_m", "height_m"] +QUAT_COLS: Final = ["qw", "qx", "qy", "qz"] +ANNO_COLS: Final = ( + ["timestamp_ns", "category"] + DIMS_COLS + QUAT_COLS + TRANSLATION_COLS +) -CUBOID_COLS: Final[List[str]] = ["tx_m", "ty_m", "tz_m", "length_m", "width_m", "height_m", "qw", "qx", "qy", "qz"] +CUBOID_COLS: Final = [ + "tx_m", + "ty_m", + "tz_m", + "length_m", + "width_m", + "height_m", + "qw", + "qx", + "qy", + "qz", +] def _get_summary_identity() -> pd.DataFrame: """Define an evaluator that compares a set of results to itself.""" - detection_cfg = DetectionCfg(categories=("REGULAR_VEHICLE",), eval_only_roi_instances=False) + detection_cfg = DetectionCfg( + categories=("REGULAR_VEHICLE",), eval_only_roi_instances=False + ) dts: pd.DataFrame = pd.read_feather(TEST_DATA_DIR / "detections_identity.feather") gts: pd.DataFrame = dts.copy() gts.loc[:, "num_interior_pts"] = np.array([1, 1, 1, 1, 1, 1]) @@ -57,7 +72,9 @@ def _get_summary_identity() -> pd.DataFrame: def _get_summary_assignment() -> pd.DataFrame: """Define an evaluator that compares a set of results to one with an extra detection to check assignment.""" - detection_cfg = DetectionCfg(categories=("REGULAR_VEHICLE",), eval_only_roi_instances=False) + detection_cfg = DetectionCfg( + categories=("REGULAR_VEHICLE",), eval_only_roi_instances=False + ) dts: pd.DataFrame = pd.read_feather(TEST_DATA_DIR / "detections_assignment.feather") gts: pd.DataFrame = pd.read_feather(TEST_DATA_DIR / "labels.feather") @@ -67,7 +84,9 @@ def _get_summary_assignment() -> pd.DataFrame: def _get_summary() -> pd.DataFrame: """Get a dummy summary.""" - detection_cfg = DetectionCfg(categories=("REGULAR_VEHICLE",), eval_only_roi_instances=False) + detection_cfg = DetectionCfg( + categories=("REGULAR_VEHICLE",), eval_only_roi_instances=False + ) dts: pd.DataFrame = pd.read_feather(TEST_DATA_DIR / "detections.feather") gts: pd.DataFrame = pd.read_feather(TEST_DATA_DIR / "labels.feather") _, _, summary = evaluate(dts, gts, detection_cfg) @@ -120,17 +139,29 @@ def test_orientation_quarter_angles() -> None: """ # Check all of the 90 degree angles expected_result: float = (2 * PI) / 4 - quarter_angles: List[NDArrayFloat] = [np.array([0, 0, angle]) for angle in np.arange(0, 2 * PI, expected_result)] + quarter_angles: List[NDArrayFloat] = [ + np.array([0, 0, angle]) for angle in np.arange(0, 2 * PI, expected_result) + ] for i in range(len(quarter_angles) - 1): - quat_xyzw_dts: NDArrayFloat = Rotation.from_rotvec(quarter_angles[i : i + 1]).as_quat() - quat_xyzw_gts: NDArrayFloat = Rotation.from_rotvec(quarter_angles[i + 1 : i + 2]).as_quat() + quat_xyzw_dts: NDArrayFloat = Rotation.from_rotvec( + quarter_angles[i : i + 1] + ).as_quat() + quat_xyzw_gts: NDArrayFloat = Rotation.from_rotvec( + quarter_angles[i + 1 : i + 2] + ).as_quat() quat_wxyz_dts = quat_xyzw_dts[..., [3, 0, 1, 2]] quat_wxyz_gts = quat_xyzw_gts[..., [3, 0, 1, 2]] - assert np.isclose(distance(quat_wxyz_dts, quat_wxyz_gts, DistanceType.ORIENTATION), expected_result) - assert np.isclose(distance(quat_wxyz_gts, quat_wxyz_dts, DistanceType.ORIENTATION), expected_result) + assert np.isclose( + distance(quat_wxyz_dts, quat_wxyz_gts, DistanceType.ORIENTATION), + expected_result, + ) + assert np.isclose( + distance(quat_wxyz_gts, quat_wxyz_dts, DistanceType.ORIENTATION), + expected_result, + ) def test_orientation_eighth_angles() -> None: @@ -140,7 +171,9 @@ def test_orientation_eighth_angles() -> None: between the detection and ground truth label. """ expected_result: float = (2 * PI) / 8 - eigth_angles: List[NDArrayFloat] = [np.array([0, 0, angle]) for angle in np.arange(0, 2 * PI, expected_result)] + eigth_angles: List[NDArrayFloat] = [ + np.array([0, 0, angle]) for angle in np.arange(0, 2 * PI, expected_result) + ] for i in range(len(eigth_angles) - 1): quat_xyzw_dts = Rotation.from_rotvec(eigth_angles[i : i + 1]).as_quat() @@ -149,8 +182,14 @@ def test_orientation_eighth_angles() -> None: quat_wxyz_dts = quat_xyzw_dts[..., [3, 0, 1, 2]] quat_wxyz_gts = quat_xyzw_gts[..., [3, 0, 1, 2]] - assert np.isclose(distance(quat_wxyz_dts, quat_wxyz_gts, DistanceType.ORIENTATION), expected_result) - assert np.isclose(distance(quat_wxyz_gts, quat_wxyz_dts, DistanceType.ORIENTATION), expected_result) + assert np.isclose( + distance(quat_wxyz_dts, quat_wxyz_gts, DistanceType.ORIENTATION), + expected_result, + ) + assert np.isclose( + distance(quat_wxyz_gts, quat_wxyz_dts, DistanceType.ORIENTATION), + expected_result, + ) def test_wrap_angle() -> None: @@ -166,7 +205,11 @@ def test_accumulate() -> None: gts: pd.DataFrame = pd.read_feather(TEST_DATA_DIR / "labels.feather") for _, group in gts.groupby(["log_id", "timestamp_ns"]): - job = (group.loc[:, CUBOID_COLS].to_numpy(), group.loc[:, CUBOID_COLS + ["num_interior_pts"]].to_numpy(), cfg) + job = ( + group.loc[:, CUBOID_COLS].to_numpy(), + group.loc[:, CUBOID_COLS + ["num_interior_pts"]].to_numpy(), + cfg, + ) dts, gts = accumulate(*job) # Check that there's a true positive under every threshold. @@ -253,7 +296,10 @@ def test_translation_error() -> None: """Test that ATE is 0 for the self-compared results.""" expected_result_identity: float = 0.0 expected_result_det: float = 0.017 # 0.1 / 6, one of six dets is off by 0.1 - assert _get_summary_identity().loc["AVERAGE_METRICS", "ATE"] == expected_result_identity + assert ( + _get_summary_identity().loc["AVERAGE_METRICS", "ATE"] + == expected_result_identity + ) assert _get_summary().loc["AVERAGE_METRICS", "ATE"] == expected_result_det @@ -261,7 +307,10 @@ def test_scale_error() -> None: """Test that ASE is 0 for the self-compared results.""" expected_result_identity: float = 0.0 expected_result_det: float = 0.033 # 0.2 / 6, one of six dets is off by 20% in IoU - assert _get_summary_identity().loc["AVERAGE_METRICS", "ASE"] == expected_result_identity + assert ( + _get_summary_identity().loc["AVERAGE_METRICS", "ASE"] + == expected_result_identity + ) assert _get_summary().loc["AVERAGE_METRICS", "ASE"] == expected_result_det @@ -270,7 +319,10 @@ def test_orientation_error() -> None: expected_result_identity = 0.0 expected_result_det = 0.524 # pi / 6, since one of six dets is off by pi - assert _get_summary_identity().loc["AVERAGE_METRICS", "AOE"] == expected_result_identity + assert ( + _get_summary_identity().loc["AVERAGE_METRICS", "AOE"] + == expected_result_identity + ) assert _get_summary().loc["AVERAGE_METRICS", "AOE"] == expected_result_det @@ -278,25 +330,73 @@ def test_compute_evaluated_dts_mask() -> None: """Unit test for computing valid detections cuboids.""" dts: NDArrayFloat = np.array( [ - [5.0, 5.0, 5.0, 1.0, 0.0, 0.0, 0.0, 3.0, 4.0, 0.0], # In bounds with at least 1 point. - [175, 175.0, 5.0, 1.0, 0.0, 0.0, 0.0, 3.0, 4.0, 0.0], # Out of bounds with at least 1 point. - [-175.0, -175.0, 5.0, 1.0, 0.0, 0.0, 0.0, 3.0, 4.0, 0.0], # Out of bounds with at least 1 point. - [1.0, 1.0, 5.0, 1.0, 0.0, 0.0, 0.0, 3.0, 4.0, 0.0], # In bounds with at least 1 point. + [ + 5.0, + 5.0, + 5.0, + 1.0, + 0.0, + 0.0, + 0.0, + 3.0, + 4.0, + 0.0, + ], # In bounds with at least 1 point. + [ + 175, + 175.0, + 5.0, + 1.0, + 0.0, + 0.0, + 0.0, + 3.0, + 4.0, + 0.0, + ], # Out of bounds with at least 1 point. + [ + -175.0, + -175.0, + 5.0, + 1.0, + 0.0, + 0.0, + 0.0, + 3.0, + 4.0, + 0.0, + ], # Out of bounds with at least 1 point. + [ + 1.0, + 1.0, + 5.0, + 1.0, + 0.0, + 0.0, + 0.0, + 3.0, + 4.0, + 0.0, + ], # In bounds with at least 1 point. ], ) - detection_cfg = DetectionCfg(categories=("REGULAR_VEHICLE",), eval_only_roi_instances=False) + detection_cfg = DetectionCfg( + categories=("REGULAR_VEHICLE",), eval_only_roi_instances=False + ) dts_mask = compute_evaluated_dts_mask(dts, detection_cfg) dts_mask_: NDArrayBool = np.array([True, False, False, True]) - np.testing.assert_array_equal(dts_mask, dts_mask_) # type: ignore + np.testing.assert_array_equal(dts_mask, dts_mask_) def test_compute_evaluated_dts_mask_2() -> None: """Randomly generate detections and ensure that they never exceed the maximum detection limit.""" - detection_cfg = DetectionCfg(categories=("REGULAR_VEHICLE",), eval_only_roi_instances=False) + detection_cfg = DetectionCfg( + categories=("REGULAR_VEHICLE",), eval_only_roi_instances=False + ) for i in range(1000): - dts: NDArrayFloat = np.random.randint(0, 250, size=(detection_cfg.max_num_dts_per_category + i, 10)).astype( - float - ) + dts: NDArrayFloat = np.random.randint( + 0, 250, size=(detection_cfg.max_num_dts_per_category + i, 10) + ).astype(float) dts_mask = compute_evaluated_dts_mask(dts, detection_cfg) assert dts_mask.sum() <= detection_cfg.max_num_dts_per_category @@ -305,19 +405,69 @@ def test_compute_evaluated_gts_mask() -> None: """Unit test for computing valid ground truth cuboids.""" gts: NDArrayFloat = np.array( [ - [5.0, 5.0, 5.0, 1.0, 0.0, 0.0, 0.0, 3.0, 4.0, 0.0, 5], # In bounds with at least 1 point. - [175, 175.0, 5.0, 1.0, 0.0, 0.0, 0.0, 3.0, 4.0, 0.0, 5], # Out of bounds with at least 1 point. - [-175.0, -175.0, 5.0, 1.0, 0.0, 0.0, 0.0, 3.0, 4.0, 0.0, 5], # Out of bounds with at least 1 point. - [1.0, 1.0, 5.0, 1.0, 0.0, 0.0, 0.0, 3.0, 4.0, 0.0, 0], # In bounds with at least 1 point. + [ + 5.0, + 5.0, + 5.0, + 1.0, + 0.0, + 0.0, + 0.0, + 3.0, + 4.0, + 0.0, + 5, + ], # In bounds with at least 1 point. + [ + 175, + 175.0, + 5.0, + 1.0, + 0.0, + 0.0, + 0.0, + 3.0, + 4.0, + 0.0, + 5, + ], # Out of bounds with at least 1 point. + [ + -175.0, + -175.0, + 5.0, + 1.0, + 0.0, + 0.0, + 0.0, + 3.0, + 4.0, + 0.0, + 5, + ], # Out of bounds with at least 1 point. + [ + 1.0, + 1.0, + 5.0, + 1.0, + 0.0, + 0.0, + 0.0, + 3.0, + 4.0, + 0.0, + 0, + ], # In bounds with at least 1 point. ], ) - detection_cfg = DetectionCfg(categories=("REGULAR_VEHICLE",), eval_only_roi_instances=False) + detection_cfg = DetectionCfg( + categories=("REGULAR_VEHICLE",), eval_only_roi_instances=False + ) gts_xyz_ego = gts[..., :3] - num_interior_pts = gts[..., -1] + num_interior_pts = gts[..., -1].astype(int) gts_mask = compute_evaluated_gts_mask(gts_xyz_ego, num_interior_pts, detection_cfg) gts_mask_: NDArrayBool = np.array([True, False, False, False]) - np.testing.assert_array_equal(gts_mask, gts_mask_) # type: ignore + np.testing.assert_array_equal(gts_mask, gts_mask_) def test_compute_objects_in_roi_mask() -> None: @@ -338,16 +488,23 @@ def test_compute_objects_in_roi_mask() -> None: "a7c8f6a2-26b6-4610-9eb3-294799f9846c", # Two vertices within ROI. ] avm = ArgoverseStaticMap.from_map_dir(map_dir, build_raster=True) - annotations = read_feather(TEST_DATA_DIR / "adcf7d18-0510-35b0-a2fa-b4cea13a6d76" / "annotations.feather") - timestamped_city_SE3_egoposes = read_city_SE3_ego(TEST_DATA_DIR / "adcf7d18-0510-35b0-a2fa-b4cea13a6d76") + annotations = read_feather( + TEST_DATA_DIR / "adcf7d18-0510-35b0-a2fa-b4cea13a6d76" / "annotations.feather" + ) + timestamped_city_SE3_egoposes = read_city_SE3_ego( + TEST_DATA_DIR / "adcf7d18-0510-35b0-a2fa-b4cea13a6d76" + ) selected_cuboids_mask = np.logical_and( - annotations.timestamp_ns == timestamp_ns, annotations["track_uuid"].isin(track_uuids) + annotations.timestamp_ns == timestamp_ns, + annotations["track_uuid"].isin(track_uuids), ) sweep_annotations = annotations.loc[selected_cuboids_mask] mask = compute_objects_in_roi_mask( - sweep_annotations.loc[:, ORDERED_CUBOID_COL_NAMES].to_numpy(), timestamped_city_SE3_egoposes[timestamp_ns], avm + sweep_annotations.loc[:, ORDERED_CUBOID_COL_NAMES].to_numpy(), + timestamped_city_SE3_egoposes[timestamp_ns], + avm, ) mask_: NDArrayBool = np.array([True, False, True]) - np.testing.assert_array_equal(mask, mask_) # type: ignore + np.testing.assert_array_equal(mask, mask_) diff --git a/tests/unit/evaluation/e2e_forecasting/__init__.py b/tests/unit/evaluation/e2e_forecasting/__init__.py new file mode 100644 index 00000000..e68d1f20 --- /dev/null +++ b/tests/unit/evaluation/e2e_forecasting/__init__.py @@ -0,0 +1 @@ +"""End-to-End Forecasting sub-package.""" diff --git a/tests/unit/evaluation/e2e_forecasting/test_eval.py b/tests/unit/evaluation/e2e_forecasting/test_eval.py new file mode 100644 index 00000000..e470deff --- /dev/null +++ b/tests/unit/evaluation/e2e_forecasting/test_eval.py @@ -0,0 +1,6 @@ +"""End-to-End Forecasting evaluation unit tests.""" + + +def test_evaluate() -> None: + """Test End-to-End Forecasting evaluation.""" + pass diff --git a/tests/unit/evaluation/scene_flow/test_sf_eval.py b/tests/unit/evaluation/scene_flow/test_sf_eval.py new file mode 100644 index 00000000..dd028fce --- /dev/null +++ b/tests/unit/evaluation/scene_flow/test_sf_eval.py @@ -0,0 +1,341 @@ +"""Scene Flow evaluation unit tests.""" + +import tempfile +from pathlib import Path + +import numpy as np + +import av2.evaluation.scene_flow.constants as constants +import av2.evaluation.scene_flow.eval as eval +from av2.evaluation.scene_flow.make_annotation_files import write_annotation +from av2.evaluation.scene_flow.utils import write_output_file +from av2.utils.typing import NDArrayBool, NDArrayFloat, NDArrayInt + +gts: NDArrayFloat = np.array( + [ + [0.0, 0.0, 0.0], + [1e-3, 0.0, 0.0], + [0.0, 1e-3, 0.0], + [0.0, 0.0, 1e-3], + [1e3, 0.0, 0.0], + [0.0, 1e3, 0.0], + [0.0, 0.0, 1e3], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ], + dtype=np.float64, +) + +dts_perfect: NDArrayFloat = gts.copy() +dts_very_close: NDArrayFloat = gts + 0.01 +dts_close: NDArrayFloat = gts + 0.05 +dts_zero: NDArrayFloat = np.zeros_like(gts).astype(np.float64) + +gts_dynamic: NDArrayBool = np.array( + [False, False, False, False, True, True, True, True, False, False] +) +gts_classes: NDArrayInt = np.array([0, 0, 0, 0, 17, 17, 1, 11, 0, 23]) +gts_valid: NDArrayBool = np.array( + [False, True, True, True, True, True, True, True, True, True] +) +gts_close: NDArrayBool = np.array( + [True, True, False, False, True, True, False, False, True, True] +) + +dts_dynamic: NDArrayBool = np.array( + [False, True, False, True, False, True, False, True, False, True] +) + + +def test_end_point_error() -> None: + """Initialize a detection and a ground truth label. + + Verify that calculated end-point error matches the expected value. + """ + epe_perfect = np.zeros(10) + epe_very_close = (np.sqrt(0.01**2 * 3, dtype=np.float64) * np.ones(10)).astype( + np.float64 + ) + epe_close = (np.sqrt(0.05**2 * 3, dtype=np.float64) * np.ones(10)).astype( + np.float64 + ) + epe_zero = np.array( + [0.0e00, 1.0e-3, 1.0e-3, 1.0e-3, 1.0e3, 1.0e3, 1.0e3, 1.0e00, 1.0e00, 1.0e00], + dtype=np.float64, + ) + + assert np.allclose(eval.compute_end_point_error(dts_perfect, gts), epe_perfect) + assert np.allclose( + eval.compute_end_point_error(dts_very_close, gts), epe_very_close + ) + assert np.allclose(eval.compute_end_point_error(dts_close, gts), epe_close) + assert np.allclose(eval.compute_end_point_error(dts_zero, gts), epe_zero) + + +def test_accuracy_strict() -> None: + """Initialize a detection and a ground truth label. + + Verify that calculated strict accuracy matches the expected value. + """ + accS_perfect = np.ones(10) + accS_close = np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0]) + accS_very_close = np.ones(10) + accS_zero = np.array([1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + + assert np.allclose(eval.compute_accuracy_strict(dts_perfect, gts), accS_perfect) + assert np.allclose( + eval.compute_accuracy_strict(dts_very_close, gts), accS_very_close + ) + assert np.allclose(eval.compute_accuracy_strict(dts_close, gts), accS_close) + assert np.allclose(eval.compute_accuracy_strict(dts_zero, gts), accS_zero) + + +def test_accuracy_relax() -> None: + """Initialize a detection and a ground truth label. + + Verify that calculated relax accuracy matches the expected value. + """ + accS_perfect = np.ones(10) + accS_close = np.ones(10) + accS_very_close = np.ones(10) + accS_zero = np.array([1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + + assert np.allclose(eval.compute_accuracy_relax(dts_perfect, gts), accS_perfect) + assert np.allclose( + eval.compute_accuracy_relax(dts_very_close, gts), accS_very_close + ) + assert np.allclose(eval.compute_accuracy_relax(dts_close, gts), accS_close) + assert np.allclose(eval.compute_accuracy_relax(dts_zero, gts), accS_zero) + + +def test_angle_error() -> None: + """Initialize a detection and a ground truth label. + + Verify that calculated angle error matches the expected value. + """ + gts_norm = np.sqrt((gts**2).sum(-1) + 0.1**2) + dts_close_norm = np.sqrt((dts_close**2).sum(-1) + 0.1**2) + dts_very_close_norm = np.sqrt((dts_very_close**2).sum(-1) + 0.1**2) + dts_perfect_norm = np.sqrt((dts_perfect**2).sum(-1) + 0.1**2) + dts_zero_norm = np.sqrt((dts_zero**2).sum(-1) + 0.1**2) + + gts_nz_value = np.array( + [0.0, 1e-3, 1e-3, 1e-3, 1e3, 1e3, 1e3, 1.0, 1.0, 1.0], dtype=np.float64 + ) + + ae_perfect = np.arccos( + np.clip( + 0.1 / dts_perfect_norm * 0.1 / gts_norm + + (gts_nz_value / gts_norm) * ((gts_nz_value + 0.00) / dts_perfect_norm), + -1.0, + 1.0, + ) + ) + ae_close = np.arccos( + 0.1 / dts_close_norm * 0.1 / gts_norm + + (gts_nz_value / gts_norm) * ((gts_nz_value + 0.05) / dts_close_norm) + ) + ae_very_close = np.arccos( + 0.1 / dts_very_close_norm * 0.1 / gts_norm + + (gts_nz_value / gts_norm) * ((gts_nz_value + 0.01) / dts_very_close_norm) + ) + ae_zero = np.arccos(0.1 / dts_zero_norm * 0.1 / gts_norm) + + assert np.allclose(eval.compute_angle_error(dts_perfect, gts), ae_perfect) + assert np.allclose(eval.compute_angle_error(dts_very_close, gts), ae_very_close) + assert np.allclose(eval.compute_angle_error(dts_close, gts), ae_close) + assert np.allclose(eval.compute_angle_error(dts_zero, gts), ae_zero) + + +def test_true_positives() -> None: + """Initialize a detection and a ground truth label. + + Verify that calculated true positive count the expected value. + """ + assert eval.compute_true_positives(dts_dynamic, gts_dynamic) == 2 + + +def test_true_negatives() -> None: + """Initialize a detection and a ground truth label. + + Verify that calculated true negative count the expected value. + """ + assert eval.compute_true_negatives(dts_dynamic, gts_dynamic) == 3 + + +def test_false_positives() -> None: + """Initialize a detection and a ground truth label. + + Verify that calculated false positive count the expected value. + """ + assert eval.compute_false_positives(dts_dynamic, gts_dynamic) == 3 + + +def test_false_negatives() -> None: + """Initialize a detection and a ground truth label. + + Verify that calculated false negative count the expected value. + """ + assert eval.compute_false_negatives(dts_dynamic, gts_dynamic) == 2 + + +def test_metrics() -> None: + """Initialize dummy ground truth labels and predictions. + + Verify that the metric breakdown has the correct subset counts and values. + """ + metrics = eval.compute_metrics( + dts_perfect, + dts_dynamic, + gts, + gts_classes, + gts_dynamic, + gts_close, + gts_valid, + constants.FOREGROUND_BACKGROUND_BREAKDOWN, + ) + num_subsets = len(list(metrics.values())[0]) + assert num_subsets == 8 + + for i in range(num_subsets): + if metrics["Class"][i] == "Background" and metrics["Motion"][i] == "Dynamic": + assert metrics["Count"][i] == 0 + if metrics["Count"][i] == 0: + assert np.isnan(metrics["EPE"][i]) + assert np.isnan(metrics["ANGLE_ERROR"][i]) + assert np.isnan(metrics["ACCURACY_STRICT"][i]) + assert np.isnan(metrics["ACCURACY_RELAX"][i]) + else: + assert not np.isnan(metrics["EPE"][i]) + assert not np.isnan(metrics["ANGLE_ERROR"][i]) + assert not np.isnan(metrics["ACCURACY_STRICT"][i]) + assert not np.isnan(metrics["ACCURACY_RELAX"][i]) + + counted_points: int = sum(metrics["Count"]) + valid_points: int = sum(gts_valid.astype(int)) + assert counted_points == valid_points + + tp: int = sum(metrics["TP"]) + assert tp == 2 # The first TN is marked invalid + fp: int = sum(metrics["FP"]) + assert fp == 3 + fn: int = sum(metrics["FN"]) + assert fn == 2 + + nz_subsets = sum([1 for count in metrics["Count"] if count > 0]) + assert nz_subsets == 5 + + assert np.allclose( + sum( + [ + accr + for accr, count in zip(metrics["ACCURACY_RELAX"], metrics["Count"]) + if count > 0 + ] + ) + / nz_subsets, + 1.0, + ) + assert np.allclose( + sum( + [ + accs + for accs, count in zip(metrics["ACCURACY_STRICT"], metrics["Count"]) + if count > 0 + ] + ) + / nz_subsets, + 1.0, + ) + assert np.allclose( + sum( + [ + ae + for ae, count in zip(metrics["ANGLE_ERROR"], metrics["Count"]) + if count > 0 + ] + ) + / nz_subsets, + 0.0, + ) + assert np.allclose( + sum([epe for epe, count in zip(metrics["EPE"], metrics["Count"]) if count > 0]) + / nz_subsets, + 0.0, + ) + + +def test_average_metrics() -> None: + """Initialize dummy ground truth labels and predictions. + + Verify that the weighted average metric breakdown has the correct subset counts and values. + """ + test_dir = Path(tempfile.TemporaryDirectory().name) + test_dir.mkdir() + anno_dir = test_dir / "annotations" + anno_dir.mkdir() + + pred_dir = test_dir / "predictions" + pred_dir.mkdir() + + timestamp_ns_1 = 111111111111111111 + timestamp_ns_2 = 111111111111111112 + + write_annotation( + gts_classes, + gts_close, + gts_dynamic, + gts_valid, + gts, + ("log", timestamp_ns_1), + anno_dir, + ) + write_annotation( + gts_classes, + gts_close, + gts_dynamic, + gts_valid, + gts, + ("log", timestamp_ns_2), + anno_dir, + ) + + write_annotation( + gts_classes, + gts_close, + gts_dynamic, + gts_valid, + gts, + ("log_missing", timestamp_ns_1), + anno_dir, + ) + + write_output_file(dts_perfect, dts_dynamic, ("log", timestamp_ns_1), pred_dir) + write_output_file(dts_perfect, dts_dynamic, ("log", timestamp_ns_2), pred_dir) + + results_df = eval.evaluate_directories(anno_dir, pred_dir) + + assert len(results_df) == 16 + assert results_df.Count.sum() == 18 + + assert np.allclose(results_df.EPE.mean(), 0.0) + assert np.allclose(results_df["ACCURACY_STRICT"].mean(), 1.0) + assert np.allclose(results_df["ACCURACY_RELAX"].mean(), 1.0) + assert np.allclose(results_df["ANGLE_ERROR"].mean(), 0.0) + + assert results_df.TP.sum() == 2 * 2 + assert results_df.TN.sum() == 2 * 2 # First true negative marked invalid + assert results_df.FP.sum() == 3 * 2 + assert results_df.FN.sum() == 2 * 2 + + assert results_df.groupby(["Class", "Motion"]).Count.sum().Background.Dynamic == 0 + results_dict = eval.results_to_dict(results_df) + + assert len(results_dict) == 38 + assert np.isnan( + [results_dict[k] for k in results_dict if k.endswith("Foreground/Static/Far")] + ).all() + assert len([True for k in results_dict if "Background/Dyamic" in k]) == 0 + assert results_dict["Dynamic IoU"] == 2 / (3 + 2 + 2) + assert results_dict["EPE 3-Way Average"] == 0.0 diff --git a/tests/unit/evaluation/scene_flow/test_sf_submission_pipeline.py b/tests/unit/evaluation/scene_flow/test_sf_submission_pipeline.py new file mode 100644 index 00000000..b0933293 --- /dev/null +++ b/tests/unit/evaluation/scene_flow/test_sf_submission_pipeline.py @@ -0,0 +1,216 @@ +"""Scene Flow evaluation unit tests.""" + +import tempfile +from pathlib import Path +from typing import Final +from zipfile import ZipFile + +import numpy as np +import pandas as pd + +import av2.evaluation.scene_flow.eval as eval +from av2.evaluation.scene_flow.example_submission import example_submission +from av2.evaluation.scene_flow.make_annotation_files import make_annotation_files +from av2.evaluation.scene_flow.make_mask_files import make_mask_files +from av2.evaluation.scene_flow.make_submission_archive import ( + make_submission_archive, + validate, +) +from av2.evaluation.scene_flow.utils import compute_eval_point_mask +from av2.torch.data_loaders.scene_flow import SceneFlowDataloader + +_TEST_DATA_ROOT: Final = Path(__file__).resolve().parent.parent.parent + + +def _zipdir(directory: Path, output_file: Path) -> None: + """Zip a directory into output_file. + + Args: + directory: The directory to recursively zip up. + output_file: The name of the output archive. + """ + with ZipFile(output_file, "w") as zf: + for f in directory.rglob("**"): + zf.write(f, arcname=str(f.relative_to(directory))) + + +def test_submission() -> None: + """Test the whole pipeline for creating masks, annotations and submission files.""" + test_dir = Path(tempfile.TemporaryDirectory().name) + test_dir.mkdir() + mask_file = test_dir / "masks.zip" + make_mask_files(str(mask_file), str(_TEST_DATA_ROOT), "test_data", "val") + + annotations_dir = test_dir / "annotations" + make_annotation_files( + str(annotations_dir), + str(mask_file), + str(_TEST_DATA_ROOT), + "test_data", + "val", + ) + + predictions_dir = test_dir / "output" + example_submission( + str(predictions_dir), str(mask_file), str(_TEST_DATA_ROOT), "test_data" + ) + + results = eval.evaluate(str(annotations_dir), str(predictions_dir)) + for metric in results: + if metric.startswith("Accuracy"): + if "Static" in metric: + assert np.allclose(results[metric], 1.0) + elif "Dynamic" in metric and "Strict" in metric: + assert np.isnan(results[metric]) or np.allclose(results[metric], 0.0) + elif metric.startswith("EPE"): + if "Static" in metric: + if "Background" in metric: + assert np.allclose(results[metric], 0.0) + elif "Foreground" in metric: + assert results[metric] < 0.06 + elif metric.startswith("Angle"): + if "Static" in metric and "Background" in metric: + assert results[metric] < 1e-4 + + output_file = test_dir / "submission.zip" + success = make_submission_archive( + str(predictions_dir), str(mask_file), str(output_file) + ) + assert success + assert output_file.stat().st_size > 0 + + annotation_files = list(annotations_dir.rglob("*.feather")) + print( + [ + anno_file.relative_to(annotations_dir).as_posix() + for anno_file in annotation_files + ] + ) + with ZipFile(output_file, "r") as zf: + files = {f.filename for f in zf.filelist} + print(files) + + results_zip = eval.results_to_dict(eval.evaluate_zip(annotations_dir, output_file)) + for metric in results: + assert np.allclose(results[metric], results_zip[metric], equal_nan=True) + + empty_predictions_dir = test_dir / "bad_output_1" + empty_predictions_dir.mkdir() + success = make_submission_archive( + str(empty_predictions_dir), str(mask_file), str(output_file) + ) + assert not success + + failed = False + try: + validate(empty_predictions_dir, mask_file) + except FileNotFoundError: + failed = True + assert failed + + # Missing a column + log_id = "7fab2350-7eaf-3b7e-a39d-6937a4c1bede" + timestamp_ns = 315966265259836000 + data_loader = SceneFlowDataloader(_TEST_DATA_ROOT, "test_data", "val") + sweep_0, sweep_1, s1_SE3_s0, _ = data_loader[0] + mask = compute_eval_point_mask((sweep_0, sweep_1, s1_SE3_s0, None)) + npts = mask.sum() + bad_df = pd.DataFrame( + { + "flow_tx_m": np.zeros(npts, dtype=np.float16), + "flow_ty_m": np.zeros(npts, dtype=np.float16), + "flow_tz_m": np.zeros(npts, dtype=np.float16), + } + ) + bad_cols_predictions_dir = test_dir / "bad_output_2" / log_id + bad_cols_predictions_dir.mkdir(parents=True, exist_ok=True) + bad_df.to_feather(bad_cols_predictions_dir / f"{timestamp_ns}.feather") + failed = False + try: + validate(bad_cols_predictions_dir.parent, mask_file) + except ValueError as e: + print(e) + assert "contain is_dynamic" in str(e) + failed = True + assert failed + + # Wrong dynamic column type + bad_df = pd.DataFrame( + { + "flow_tx_m": np.zeros(npts, dtype=np.float16), + "flow_ty_m": np.zeros(npts, dtype=np.float16), + "flow_tz_m": np.zeros(npts, dtype=np.float16), + "is_dynamic": np.zeros(npts, dtype=np.float16), + } + ) + bad_type_predictions_dir = test_dir / "bad_output_3" / log_id + bad_type_predictions_dir.mkdir(parents=True, exist_ok=True) + bad_df.to_feather(bad_type_predictions_dir / f"{timestamp_ns}.feather") + failed = False + try: + validate(bad_type_predictions_dir.parent, mask_file) + except ValueError as e: + assert "column is_dynamic" in str(e) + failed = True + assert failed + + # Wrong flow column type + bad_df = pd.DataFrame( + { + "flow_tx_m": np.zeros(npts, dtype=np.float16), + "flow_ty_m": np.zeros(npts, dtype=np.float16), + "flow_tz_m": np.zeros(npts, dtype=np.float32), + "is_dynamic": np.zeros(npts, dtype=bool), + } + ) + bad_type_2_predictions_dir = test_dir / "bad_output_4" / log_id + bad_type_2_predictions_dir.mkdir(exist_ok=True, parents=True) + bad_df.to_feather(bad_type_2_predictions_dir / f"{timestamp_ns}.feather") + failed = False + try: + validate(bad_type_2_predictions_dir.parent, mask_file) + except ValueError as e: + assert "column flow_tz_m" in str(e) + failed = True + assert failed + + # extra column + bad_df = pd.DataFrame( + { + "flow_tx_m": np.zeros(npts, dtype=np.float16), + "flow_ty_m": np.zeros(npts, dtype=np.float16), + "flow_tz_m": np.zeros(npts, dtype=np.float16), + "is_dynamic": np.zeros(npts, dtype=bool), + "is_static": np.zeros(npts, dtype=bool), + } + ) + extra_col_predictions_dir = test_dir / "bad_output_5" / log_id + extra_col_predictions_dir.mkdir(parents=True, exist_ok=True) + bad_df.to_feather(extra_col_predictions_dir / f"{timestamp_ns}.feather") + failed = False + try: + validate(extra_col_predictions_dir.parent, mask_file) + except ValueError as e: + assert "extra" in str(e) + failed = True + assert failed + + # wrong length + bad_df = pd.DataFrame( + { + "flow_tx_m": np.zeros(npts + 1, dtype=np.float16), + "flow_ty_m": np.zeros(npts + 1, dtype=np.float16), + "flow_tz_m": np.zeros(npts + 1, dtype=np.float16), + "is_dynamic": np.zeros(npts + 1, dtype=bool), + } + ) + wrong_len_predictions_dir = test_dir / "bad_output_6" / log_id + wrong_len_predictions_dir.mkdir(exist_ok=True, parents=True) + bad_df.to_feather(wrong_len_predictions_dir / f"{timestamp_ns}.feather") + failed = False + try: + validate(wrong_len_predictions_dir.parent, mask_file) + except ValueError as e: + assert "rows" in str(e) + failed = True + assert failed diff --git a/tests/geometry/data/b87683ae-14c5-321f-8af3-623e7bafc3a7/annotations.feather b/tests/unit/geometry/data/b87683ae-14c5-321f-8af3-623e7bafc3a7/annotations.feather similarity index 100% rename from tests/geometry/data/b87683ae-14c5-321f-8af3-623e7bafc3a7/annotations.feather rename to tests/unit/geometry/data/b87683ae-14c5-321f-8af3-623e7bafc3a7/annotations.feather diff --git a/tests/geometry/test_geometry.py b/tests/unit/geometry/test_geometry.py similarity index 77% rename from tests/geometry/test_geometry.py rename to tests/unit/geometry/test_geometry.py index a6330b8e..141f0c86 100644 --- a/tests/geometry/test_geometry.py +++ b/tests/unit/geometry/test_geometry.py @@ -47,7 +47,7 @@ def test_wrap_angles(yaw_1: float, yaw_2: float, expected_error_deg: float) -> N def test_cart_to_hom_2d() -> None: - """Convert 2d cartesian coordinates to homogenous, and back again.""" + """Convert 2d cartesian coordinates to homogeneous, and back again.""" cart: NDArrayFloat = np.arange(16 * 2).reshape(16, 2).astype(np.float64) hom = geometry_utils.cart_to_hom(cart=cart) @@ -57,7 +57,7 @@ def test_cart_to_hom_2d() -> None: def test_cart_to_hom_3d() -> None: - """Convert 3d cartesian coordinates to homogenous, and back again.""" + """Convert 3d cartesian coordinates to homogeneous, and back again.""" cart: NDArrayFloat = np.arange(16 * 3).reshape(16, 3).astype(np.float64) hom = geometry_utils.cart_to_hom(cart=cart) @@ -82,9 +82,14 @@ def test_cart_to_hom_3d() -> None: np.array([[1, -1], [1, -2], [0, -2], [0, -1]]).astype(np.float64), ), ], - ids=[f"Cartesian to texture coordinates conversion (Test Case: {idx + 1})" for idx in range(2)], + ids=[ + f"Cartesian to texture coordinates conversion (Test Case: {idx + 1})" + for idx in range(2) + ], ) -def test_xy_to_uv(xy: NDArrayFloat, width: int, height: int, expected_uv: NDArrayFloat) -> None: +def test_xy_to_uv( + xy: NDArrayFloat, width: int, height: int, expected_uv: NDArrayFloat +) -> None: """Test conversion of coordinates in R^2 (x,y) to texture coordinates (u,v) in R^2. Args: @@ -114,7 +119,7 @@ def test_quat_to_mat_3d(quat_wxyz: NDArrayFloat) -> None: # (Note: For comparison, Quaternion needs to be converted from scalar last to scalar first.) quat_to_quat: Callable[[NDArrayFloat], Any] = lambda quat_wxyz: Rotation.as_quat( Rotation.from_matrix(geometry_utils.quat_to_mat(quat_wxyz)) - )[..., [3, 0, 1, 2]] + )[..., [3, 0, 1, 2]].astype(np.float64) assert np.allclose(quat_wxyz, quat_to_quat(quat_wxyz)) @@ -122,15 +127,24 @@ def test_quat_to_mat_3d(quat_wxyz: NDArrayFloat) -> None: @pytest.mark.parametrize( "cart_xyz, expected_sph_theta_phi_r", [ - (np.array([1, 1, 1]).astype(np.float64), np.array([0.78539816, 0.61547971, 1.73205081])), + ( + np.array([1, 1, 1]).astype(np.float64), + np.array([0.78539816, 0.61547971, 1.73205081]), + ), ( np.array([[1, 1, 1], [1, 2, 0]]).astype(np.float64), - np.array([[0.78539816, 0.61547971, 1.73205081], [1.10714872, 0.0, 2.23606798]]), + np.array( + [[0.78539816, 0.61547971, 1.73205081], [1.10714872, 0.0, 2.23606798]] + ), ), ], - ids=[f"Cartesian to Spherical coodinates (Test Case: {idx + 1})" for idx in range(2)], + ids=[ + f"Cartesian to Spherical coordinates (Test Case: {idx + 1})" for idx in range(2) + ], ) -def test_cart_to_sph_3d(cart_xyz: NDArrayFloat, expected_sph_theta_phi_r: NDArrayFloat) -> None: +def test_cart_to_sph_3d( + cart_xyz: NDArrayFloat, expected_sph_theta_phi_r: NDArrayFloat +) -> None: """Test conversion of cartesian coordinates to spherical coordinates. Args: @@ -144,38 +158,83 @@ def test_cart_to_sph_3d(cart_xyz: NDArrayFloat, expected_sph_theta_phi_r: NDArra "points_xyz, lower_bound_inclusive, upper_bound_exclusive, expected_crop_points, expected_mask", [ ( - np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1], [1, 0, 1], [1, 1, 1]]).astype( - np.int64 - ), + np.array( + [ + [0, 0, 0], + [1, 0, 0], + [1, 1, 0], + [0, 1, 0], + [0, 1, 1], + [0, 0, 1], + [1, 0, 1], + [1, 1, 1], + ] + ).astype(np.int64), (0, 0, 0), (1.5, 1.5, 1.5), - np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1], [1, 0, 1], [1, 1, 1]]).astype( - np.int64 - ), + np.array( + [ + [0, 0, 0], + [1, 0, 0], + [1, 1, 0], + [0, 1, 0], + [0, 1, 1], + [0, 0, 1], + [1, 0, 1], + [1, 1, 1], + ] + ).astype(np.int64), np.array([True] * 8), ), ( - np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1], [1, 0, 1], [1, 1, 1]]).astype( - np.int64 - ), + np.array( + [ + [0, 0, 0], + [1, 0, 0], + [1, 1, 0], + [0, 1, 0], + [0, 1, 1], + [0, 0, 1], + [1, 0, 1], + [1, 1, 1], + ] + ).astype(np.int64), (0, 0, 0), (0.5, 0.5, 0.5), np.array([[0, 0, 0]]).astype(np.int64), np.array([True] + [False] * 7), ), ( - np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1], [1, 0, 1], [1, 1, 1]]).astype( - np.int64 - ), + np.array( + [ + [0, 0, 0], + [1, 0, 0], + [1, 1, 0], + [0, 1, 0], + [0, 1, 1], + [0, 0, 1], + [1, 0, 1], + [1, 1, 1], + ] + ).astype(np.int64), (0, 0, 0), (1.25, 1.25, 1.0), np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]).astype(np.int64), np.array([True] * 4 + [False] * 4), ), ( - np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1], [1, 0, 1], [1, 1, 1]]).astype( - np.int64 - ), + np.array( + [ + [0, 0, 0], + [1, 0, 0], + [1, 1, 0], + [0, 1, 0], + [0, 1, 1], + [0, 0, 1], + [1, 0, 1], + [1, 1, 1], + ] + ).astype(np.int64), (-1.0, -1.0, -1.0), (0.0, 1.0, 1.0), np.empty((0, 3)).astype(np.int64), @@ -205,10 +264,12 @@ def test_crop_points( expected_crop_points: (...,3) Expected tuple of cropped Cartesian coordinates. expected_mask: (...,) Expected boolean mask. """ - cropped_xyz, mask = geometry_utils.crop_points(points_xyz, lower_bound_inclusive, upper_bound_exclusive) + cropped_xyz, mask = geometry_utils.crop_points( + points_xyz, lower_bound_inclusive, upper_bound_exclusive + ) - np.testing.assert_array_equal(expected_crop_points, cropped_xyz) # type: ignore - np.testing.assert_array_equal(expected_mask, mask) # type: ignore + np.testing.assert_array_equal(expected_crop_points, cropped_xyz) + np.testing.assert_array_equal(expected_mask, mask) @pytest.mark.parametrize( @@ -267,7 +328,9 @@ def test_crop_points( "No points lie inside the bounding box.", ], ) -def test_compute_interior_points_mask(points_xyz: NDArrayFloat, expected_is_interior: NDArrayBool) -> None: +def test_compute_interior_points_mask( + points_xyz: NDArrayFloat, expected_is_interior: NDArrayBool +) -> None: r"""Test finding the points interior to an axis-aligned cuboid. Reference: https://math.stackexchange.com/questions/1472049/check-if-a-point-is-inside-a-rectangular-shaped-area-3d @@ -300,11 +363,15 @@ def test_compute_interior_points_mask(points_xyz: NDArrayFloat, expected_is_inte category=AnnotationCategories.REGULAR_VEHICLE, timestamp_ns=0, ) - is_interior = geometry_utils.compute_interior_points_mask(points_xyz, cuboid.vertices_m) + is_interior = geometry_utils.compute_interior_points_mask( + points_xyz, cuboid.vertices_m + ) assert np.array_equal(is_interior, expected_is_interior) -def test_benchmark_compute_interior_points_mask_optimized(benchmark: Callable[..., Any]) -> None: +def test_benchmark_compute_interior_points_mask_optimized( + benchmark: Callable[..., Any] +) -> None: """Benchmark compute_interior_pts on 100000 random points.""" rotation: NDArrayFloat = np.eye(3) translation: NDArrayFloat = np.array([0.5, 0.5, 0.5]) @@ -320,10 +387,14 @@ def test_benchmark_compute_interior_points_mask_optimized(benchmark: Callable[.. N = 100000 points_xyz: NDArrayFloat = 100.0 * np.random.rand(N, 3) - benchmark(geometry_utils.compute_interior_points_mask, points_xyz, cuboid.vertices_m) + benchmark( + geometry_utils.compute_interior_points_mask, points_xyz, cuboid.vertices_m + ) -def test_benchmark_compute_interior_points_mask_slow(benchmark: Callable[..., Any]) -> None: +def test_benchmark_compute_interior_points_mask_slow( + benchmark: Callable[..., Any] +) -> None: """Benchmark compute_interior_points_mask on 100000 random points.""" rotation: NDArrayFloat = np.eye(3) translation: NDArrayFloat = np.array([0.5, 0.5, 0.5]) @@ -340,7 +411,9 @@ def test_benchmark_compute_interior_points_mask_slow(benchmark: Callable[..., An N = 100000 points_xyz: NDArrayFloat = 100.0 * np.random.rand(N, 3) - def compute_interior_points_mask_slow(points_xyz: NDArrayFloat, cuboid_vertices: NDArrayFloat) -> NDArrayBool: + def compute_interior_points_mask_slow( + points_xyz: NDArrayFloat, cuboid_vertices: NDArrayFloat + ) -> NDArrayBool: """Compute the interior points mask with the older slow version. Args: @@ -357,30 +430,38 @@ def compute_interior_points_mask_slow(points_xyz: NDArrayFloat, cuboid_vertices: # point x lies within the box when the following # constraints are respected valid_u1 = np.logical_and( - u.dot(cuboid_vertices[2]) <= points_xyz.dot(u), points_xyz.dot(u) <= u.dot(cuboid_vertices[6]) + u.dot(cuboid_vertices[2]) <= points_xyz.dot(u), + points_xyz.dot(u) <= u.dot(cuboid_vertices[6]), ) valid_v1 = np.logical_and( - v.dot(cuboid_vertices[2]) <= points_xyz.dot(v), points_xyz.dot(v) <= v.dot(cuboid_vertices[3]) + v.dot(cuboid_vertices[2]) <= points_xyz.dot(v), + points_xyz.dot(v) <= v.dot(cuboid_vertices[3]), ) valid_w1 = np.logical_and( - w.dot(cuboid_vertices[2]) <= points_xyz.dot(w), points_xyz.dot(w) <= w.dot(cuboid_vertices[1]) + w.dot(cuboid_vertices[2]) <= points_xyz.dot(w), + points_xyz.dot(w) <= w.dot(cuboid_vertices[1]), ) valid_u2 = np.logical_and( - u.dot(cuboid_vertices[2]) >= points_xyz.dot(u), points_xyz.dot(u) >= u.dot(cuboid_vertices[6]) + u.dot(cuboid_vertices[2]) >= points_xyz.dot(u), + points_xyz.dot(u) >= u.dot(cuboid_vertices[6]), ) valid_v2 = np.logical_and( - v.dot(cuboid_vertices[2]) >= points_xyz.dot(v), points_xyz.dot(v) >= v.dot(cuboid_vertices[3]) + v.dot(cuboid_vertices[2]) >= points_xyz.dot(v), + points_xyz.dot(v) >= v.dot(cuboid_vertices[3]), ) valid_w2 = np.logical_and( - w.dot(cuboid_vertices[2]) >= points_xyz.dot(w), points_xyz.dot(w) >= w.dot(cuboid_vertices[1]) + w.dot(cuboid_vertices[2]) >= points_xyz.dot(w), + points_xyz.dot(w) >= w.dot(cuboid_vertices[1]), ) valid_u = np.logical_or(valid_u1, valid_u2) valid_v = np.logical_or(valid_v1, valid_v2) valid_w = np.logical_or(valid_w1, valid_w2) - is_interior: NDArrayBool = np.logical_and(np.logical_and(valid_u, valid_v), valid_w) + is_interior: NDArrayBool = np.logical_and( + np.logical_and(valid_u, valid_v), valid_w + ) return is_interior benchmark(compute_interior_points_mask_slow, points_xyz, cuboid.vertices_m) @@ -395,7 +476,7 @@ def test_xyz_to_mat_matrix() -> None: [0.5000000, 0.8535534, -0.1464466], [-0.7071068, 0.5000000, 0.5000000], ] - np.testing.assert_allclose(rotation_matrix, rotation_matrix_expected) # type: ignore + np.testing.assert_allclose(rotation_matrix, rotation_matrix_expected) def test_xyz_to_mat_round_trip() -> None: @@ -403,7 +484,7 @@ def test_xyz_to_mat_round_trip() -> None: tait_bryan_angles: NDArrayFloat = np.deg2rad([45.0, 45.0, 45.0]) rotation_matrix = xyz_to_mat(tait_bryan_angles) tait_bryan_angles_ = mat_to_xyz(rotation_matrix) - np.testing.assert_allclose(tait_bryan_angles, tait_bryan_angles_) # type: ignore + np.testing.assert_allclose(tait_bryan_angles, tait_bryan_angles_) def test_mat_to_xyz_round_trip() -> None: @@ -417,7 +498,7 @@ def test_mat_to_xyz_round_trip() -> None: ) tait_bryan_angles = mat_to_xyz(rotation_matrix) rotation_matrix_ = xyz_to_mat(tait_bryan_angles) - np.testing.assert_allclose(rotation_matrix, rotation_matrix_, atol=1e-10) # type: ignore + np.testing.assert_allclose(rotation_matrix, rotation_matrix_, atol=1e-10) def test_mat_to_xyz_constrained() -> None: @@ -433,9 +514,11 @@ def test_mat_to_xyz_constrained() -> None: xyz[0] = 0 # Set roll to zero. mat_constrained = xyz_to_mat(xyz) - xyz_expected = np.deg2rad([0, 0, 90]) # [45, 0, 90] -> constrain roll to zero -> [0, 0, 90]. + xyz_expected = np.deg2rad( + [0, 0, 90] + ) # [45, 0, 90] -> constrain roll to zero -> [0, 0, 90]. mat_constrained_expected = xyz_to_mat(xyz_expected) - np.testing.assert_allclose(mat_constrained, mat_constrained_expected) # type: ignore + np.testing.assert_allclose(mat_constrained, mat_constrained_expected) def RzRyRx(x: float, y: float, z: float) -> NDArrayFloat: @@ -494,7 +577,6 @@ def test_xyz_to_mat_vs_gtsam() -> None: """Compare our implementation (using Scipy) vs. the GTSAM derivation.""" num_iters = 10000 for _ in range(num_iters): - # in [-pi, pi] x = 2 * np.pi * (np.random.rand() - 0.5) z = 2 * np.pi * (np.random.rand() - 0.5) @@ -511,7 +593,12 @@ def test_xyz_to_mat_vs_gtsam() -> None: def test_constrain_cuboid_pose() -> None: """Unit test to constrain cuboid pose.""" - path = Path(__file__).parent.resolve() / "data" / "b87683ae-14c5-321f-8af3-623e7bafc3a7" / "annotations.feather" + path = ( + Path(__file__).parent.resolve() + / "data" + / "b87683ae-14c5-321f-8af3-623e7bafc3a7" + / "annotations.feather" + ) cuboid_list = CuboidList.from_feather(path) for cuboid in cuboid_list.cuboids: pose = cuboid.dst_SE3_object diff --git a/tests/geometry/test_interpolate.py b/tests/unit/geometry/test_interpolate.py similarity index 87% rename from tests/geometry/test_interpolate.py rename to tests/unit/geometry/test_interpolate.py index aad85044..45a42c33 100644 --- a/tests/geometry/test_interpolate.py +++ b/tests/unit/geometry/test_interpolate.py @@ -91,8 +91,12 @@ def test_compute_lane_width_curved_width1() -> None: \\ ----- \\----- """ - left_even_pts: NDArrayFloat = np.array([[0, 2], [-2, 2], [-3, 1], [-3, 0], [-2, -1], [0, -1]]) - right_even_pts: NDArrayFloat = np.array([[0, 3], [-2, 3], [-4, 1], [-4, 0], [-2, -2], [0, -2]]) + left_even_pts: NDArrayFloat = np.array( + [[0, 2], [-2, 2], [-3, 1], [-3, 0], [-2, -1], [0, -1]] + ) + right_even_pts: NDArrayFloat = np.array( + [[0, 3], [-2, 3], [-4, 1], [-4, 0], [-2, -2], [0, -2]] + ) lane_width = interp_utils.compute_lane_width(left_even_pts, right_even_pts) gt_lane_width = 1.0 assert np.isclose(lane_width, gt_lane_width) @@ -115,9 +119,13 @@ def test_compute_lane_width_curved_not_width1() -> None: We get waypoint distances of [1,1,1,1,0.707..., 1,1] """ - left_even_pts: NDArrayFloat = np.array([[0, 2], [-2, 2], [-3, 1], [-3, 0], [-2.5, -0.5], [-2, -1], [0, -1]]) + left_even_pts: NDArrayFloat = np.array( + [[0, 2], [-2, 2], [-3, 1], [-3, 0], [-2.5, -0.5], [-2, -1], [0, -1]] + ) - right_even_pts: NDArrayFloat = np.array([[0, 3], [-2, 3], [-4, 1], [-4, 0], [-3, -1], [-2, -2], [0, -2]]) + right_even_pts: NDArrayFloat = np.array( + [[0, 3], [-2, 3], [-4, 1], [-4, 0], [-3, -1], [-2, -2], [0, -2]] + ) lane_width = interp_utils.compute_lane_width(left_even_pts, right_even_pts) gt_lane_width = 0.9581581115980783 @@ -236,7 +244,9 @@ def test_compute_mid_pivot_arc_5pt_cul_de_sac() -> None: # centerline_pts: Numpy array of shape (N,3) centerline_pts, lane_width = interp_utils.compute_mid_pivot_arc(single_pt, arc_pts) - gt_centerline_pts: NDArrayFloat = np.array([[0, 1], [0.5, 0.5], [1, 0], [0.5, -0.5], [0, -1]]) + gt_centerline_pts: NDArrayFloat = np.array( + [[0, 1], [0.5, 0.5], [1, 0], [0.5, -0.5], [0, -1]] + ) gt_lane_width = (2 + 2 + 2 + np.sqrt(2) + np.sqrt(2)) / 5 assert np.allclose(centerline_pts, gt_centerline_pts) assert np.isclose(lane_width, gt_lane_width) @@ -251,9 +261,13 @@ def test_compute_midpoint_line_cul_de_sac_right_onept() -> None: left_ln_bnds: NDArrayFloat = np.array([[0, 2], [1, 1], [2, 0], [1, -1], [0, -2]]) right_ln_bnds: NDArrayFloat = np.array([[0, 0]]) - centerline_pts, lane_width = interp_utils.compute_midpoint_line(left_ln_bnds, right_ln_bnds, num_interp_pts=5) + centerline_pts, lane_width = interp_utils.compute_midpoint_line( + left_ln_bnds, right_ln_bnds, num_interp_pts=5 + ) - gt_centerline_pts: NDArrayFloat = np.array([[0, 1], [0.5, 0.5], [1, 0], [0.5, -0.5], [0, -1]]) + gt_centerline_pts: NDArrayFloat = np.array( + [[0, 1], [0.5, 0.5], [1, 0], [0.5, -0.5], [0, -1]] + ) gt_lane_width = (2 + 2 + 2 + np.sqrt(2) + np.sqrt(2)) / 5 assert np.allclose(centerline_pts, gt_centerline_pts) @@ -269,9 +283,13 @@ def test_compute_midpoint_line_cul_de_sac_left_onept() -> None: right_ln_bnds: NDArrayFloat = np.array([[0, 2], [1, 1], [2, 0], [1, -1], [0, -2]]) left_ln_bnds: NDArrayFloat = np.array([[0, 0]]) - centerline_pts, lane_width = interp_utils.compute_midpoint_line(left_ln_bnds, right_ln_bnds, num_interp_pts=5) + centerline_pts, lane_width = interp_utils.compute_midpoint_line( + left_ln_bnds, right_ln_bnds, num_interp_pts=5 + ) - gt_centerline_pts: NDArrayFloat = np.array([[0, 1], [0.5, 0.5], [1, 0], [0.5, -0.5], [0, -1]]) + gt_centerline_pts: NDArrayFloat = np.array( + [[0, 1], [0.5, 0.5], [1, 0], [0.5, -0.5], [0, -1]] + ) gt_lane_width = (2 + 2 + 2 + np.sqrt(2) + np.sqrt(2)) / 5 assert np.allclose(centerline_pts, gt_centerline_pts) @@ -284,12 +302,18 @@ def test_compute_midpoint_line_straightline_maintain_5_waypts() -> None: Make sure that if we provide left and right boundary polylines in 2d, we can get the correct centerline by averaging left and right waypoints. """ - right_ln_bnds: NDArrayFloat = np.array([[-1, 4], [-1, 2], [-1, 0], [-1, -2], [-1, -4]]) + right_ln_bnds: NDArrayFloat = np.array( + [[-1, 4], [-1, 2], [-1, 0], [-1, -2], [-1, -4]] + ) left_ln_bnds: NDArrayFloat = np.array([[2, 4], [2, 2], [2, 0], [2, -2], [2, -4]]) - centerline_pts, lane_width = interp_utils.compute_midpoint_line(left_ln_bnds, right_ln_bnds, num_interp_pts=5) + centerline_pts, lane_width = interp_utils.compute_midpoint_line( + left_ln_bnds, right_ln_bnds, num_interp_pts=5 + ) - gt_centerline_pts: NDArrayFloat = np.array([[0.5, 4], [0.5, 2], [0.5, 0], [0.5, -2], [0.5, -4]]) + gt_centerline_pts: NDArrayFloat = np.array( + [[0.5, 4], [0.5, 2], [0.5, 0], [0.5, -2], [0.5, -4]] + ) gt_lane_width = 3.0 assert np.allclose(centerline_pts, gt_centerline_pts) assert np.isclose(lane_width, gt_lane_width) @@ -301,12 +325,18 @@ def test_compute_midpoint_line_straightline_maintain_4_waypts() -> None: Make sure that if we provide left and right boundary polylines in 2d, we can get the correct centerline by averaging left and right waypoints. """ - right_ln_bnds: NDArrayFloat = np.array([[-1, 4], [-1, 2], [-1, 0], [-1, -2], [-1, -4]]) + right_ln_bnds: NDArrayFloat = np.array( + [[-1, 4], [-1, 2], [-1, 0], [-1, -2], [-1, -4]] + ) left_ln_bnds: NDArrayFloat = np.array([[2, 4], [2, 2], [2, 0], [2, -2], [2, -4]]) - centerline_pts, lane_width = interp_utils.compute_midpoint_line(left_ln_bnds, right_ln_bnds, num_interp_pts=4) + centerline_pts, lane_width = interp_utils.compute_midpoint_line( + left_ln_bnds, right_ln_bnds, num_interp_pts=4 + ) - gt_centerline_pts: NDArrayFloat = np.array([[0.5, 4], [0.5, 4 / 3], [0.5, -4 / 3], [0.5, -4]]) + gt_centerline_pts: NDArrayFloat = np.array( + [[0.5, 4], [0.5, 4 / 3], [0.5, -4 / 3], [0.5, -4]] + ) gt_lane_width = 3.0 assert np.allclose(centerline_pts, gt_centerline_pts) assert np.isclose(lane_width, gt_lane_width) @@ -318,10 +348,14 @@ def test_compute_midpoint_line_straightline_maintain_3_waypts() -> None: Make sure that if we provide left and right boundary polylines in 2d, we can get the correct centerline by averaging left and right waypoints. """ - right_ln_bnds: NDArrayFloat = np.array([[-1, 4], [-1, 2], [-1, 0], [-1, -2], [-1, -4]]) + right_ln_bnds: NDArrayFloat = np.array( + [[-1, 4], [-1, 2], [-1, 0], [-1, -2], [-1, -4]] + ) left_ln_bnds: NDArrayFloat = np.array([[2, 4], [2, 2], [2, 0], [2, -2], [2, -4]]) - centerline_pts, lane_width = interp_utils.compute_midpoint_line(left_ln_bnds, right_ln_bnds, num_interp_pts=3) + centerline_pts, lane_width = interp_utils.compute_midpoint_line( + left_ln_bnds, right_ln_bnds, num_interp_pts=3 + ) gt_centerline_pts: NDArrayFloat = np.array([[0.5, 4], [0.5, 0], [0.5, -4]]) gt_lane_width = 3.0 @@ -335,10 +369,14 @@ def test_compute_midpoint_line_straightline_maintain_2_waypts() -> None: Make sure that if we provide left and right boundary polylines in 2d, we can get the correct centerline by averaging left and right waypoints. """ - right_ln_bnds: NDArrayFloat = np.array([[-1, 4], [-1, 2], [-1, 0], [-1, -2], [-1, -4]]) + right_ln_bnds: NDArrayFloat = np.array( + [[-1, 4], [-1, 2], [-1, 0], [-1, -2], [-1, -4]] + ) left_ln_bnds: NDArrayFloat = np.array([[2, 4], [2, 2], [2, 0], [2, -2], [2, -4]]) - centerline_pts, lane_width = interp_utils.compute_midpoint_line(left_ln_bnds, right_ln_bnds, num_interp_pts=2) + centerline_pts, lane_width = interp_utils.compute_midpoint_line( + left_ln_bnds, right_ln_bnds, num_interp_pts=2 + ) gt_centerline_pts: NDArrayFloat = np.array([[0.5, 4], [0.5, -4]]) gt_lane_width = 3.0 @@ -358,7 +396,9 @@ def test_compute_midpoint_line_curved_maintain_4_waypts() -> None: right_ln_bnds: NDArrayFloat = np.array([[-1, 3], [1, 3], [4, 0], [4, -2]]) left_ln_bnds: NDArrayFloat = np.array([[-1, 1], [1, 1], [2, 0], [2, -2]]) - centerline_pts, lane_width = interp_utils.compute_midpoint_line(left_ln_bnds, right_ln_bnds, num_interp_pts=4) + centerline_pts, lane_width = interp_utils.compute_midpoint_line( + left_ln_bnds, right_ln_bnds, num_interp_pts=4 + ) # from argoverse.utils.mpl_plotting_utils import draw_polygon_mpl @@ -403,7 +443,9 @@ def test_compute_midpoint_line_straightline_maintain_3_waypts_3dpolylines() -> N ) # fmt: on - centerline_pts, lane_width = interp_utils.compute_midpoint_line(left_ln_bnds, right_ln_bnds, num_interp_pts=3) + centerline_pts, lane_width = interp_utils.compute_midpoint_line( + left_ln_bnds, right_ln_bnds, num_interp_pts=3 + ) # fmt: off gt_centerline_pts: NDArrayFloat = np.array( [ @@ -558,14 +600,17 @@ def test_interpolate_pose() -> None: city_SE3_egot0 = SE3(rotation=np.eye(3), translation=np.array([5, 0, 0])) city_SE3_egot1 = SE3( - rotation=Rotation.from_euler("z", 90, degrees=True).as_matrix(), translation=np.array([0, 5, 0]) + rotation=Rotation.from_euler("z", 90, degrees=True).as_matrix(), + translation=np.array([0, 5, 0]), ) t0 = 0 t1 = 10 for query_timestamp in np.arange(11): pose = interp_utils.interpolate_pose( - key_timestamps=(t0, t1), key_poses=(city_SE3_egot0, city_SE3_egot1), query_timestamp=query_timestamp + key_timestamps=(t0, t1), + key_poses=(city_SE3_egot0, city_SE3_egot1), + query_timestamp=query_timestamp, ) if visualize: _plot_pose(pose) @@ -608,16 +653,22 @@ def test_linear_interpolation() -> None: X1: NDArrayFloat = np.array([-1, 2, 10], dtype=float) # at start of interval (@5 sec) - Xt_5 = interp_utils.linear_interpolation(key_timestamps=(5, 15), key_translations=(X0, X1), query_timestamp=5) + Xt_5 = interp_utils.linear_interpolation( + key_timestamps=(5, 15), key_translations=(X0, X1), query_timestamp=5 + ) expected_Xt_5: NDArrayFloat = np.array([1, 0, 0], dtype=float) assert np.array_equal(Xt_5, expected_Xt_5) # midway through interval (@10 sec) - Xt_10 = interp_utils.linear_interpolation(key_timestamps=(5, 15), key_translations=(X0, X1), query_timestamp=10) + Xt_10 = interp_utils.linear_interpolation( + key_timestamps=(5, 15), key_translations=(X0, X1), query_timestamp=10 + ) expected_Xt_10: NDArrayFloat = np.array([0, 1, 5], dtype=float) assert np.array_equal(Xt_10, expected_Xt_10) # at end of interval (@15 sec) - Xt_15 = interp_utils.linear_interpolation(key_timestamps=(5, 15), key_translations=(X0, X1), query_timestamp=15) + Xt_15 = interp_utils.linear_interpolation( + key_timestamps=(5, 15), key_translations=(X0, X1), query_timestamp=15 + ) expected_Xt_15: NDArrayFloat = np.array([-1, 2, 10], dtype=float) assert np.array_equal(Xt_15, expected_Xt_15) diff --git a/tests/geometry/test_pinhole_camera.py b/tests/unit/geometry/test_pinhole_camera.py similarity index 82% rename from tests/geometry/test_pinhole_camera.py rename to tests/unit/geometry/test_pinhole_camera.py index b1a16667..c61c2ab7 100644 --- a/tests/geometry/test_pinhole_camera.py +++ b/tests/unit/geometry/test_pinhole_camera.py @@ -30,12 +30,21 @@ def _create_pinhole_camera( translation: NDArrayFloat = np.zeros(3) ego_SE3_cam = SE3(rotation=rotation, translation=translation) - intrinsics = Intrinsics(fx_px=fx_px, fy_px=fy_px, cx_px=cx_px, cy_px=cy_px, width_px=width_px, height_px=height_px) + intrinsics = Intrinsics( + fx_px=fx_px, + fy_px=fy_px, + cx_px=cx_px, + cy_px=cy_px, + width_px=width_px, + height_px=height_px, + ) pinhole_camera = PinholeCamera(ego_SE3_cam, intrinsics, cam_name) return pinhole_camera -def _fit_plane_to_point_cloud(points_xyz: NDArrayFloat) -> Tuple[float, float, float, float]: +def _fit_plane_to_point_cloud( + points_xyz: NDArrayFloat, +) -> Tuple[float, float, float, float]: """Use SVD with at least 3 points to fit a plane. Args: @@ -45,12 +54,14 @@ def _fit_plane_to_point_cloud(points_xyz: NDArrayFloat) -> Tuple[float, float, f (4,) Plane coefficients. Defining ax + by + cz = d for the plane. """ center_xyz: NDArrayFloat = np.mean(points_xyz, axis=0) - out: Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat] = np.linalg.svd(points_xyz - center_xyz) # type: ignore + out: Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat] = np.linalg.svd( + points_xyz - center_xyz + ) vh = out[2] # Get the unitary normal vector a, b, c = float(vh[2, 0]), float(vh[2, 1]), float(vh[2, 2]) - d: float = -np.dot([a, b, c], center_xyz) # type: ignore + d: float = -np.dot([a, b, c], center_xyz) return (a, b, c, d) @@ -63,8 +74,17 @@ def test_intrinsics_constructor() -> None: cx_px, cy_px = 1024, 775 - intrinsics = Intrinsics(fx_px=fx_px, fy_px=fy_px, cx_px=cx_px, cy_px=cy_px, width_px=width_px, height_px=height_px) - K_expected: NDArrayFloat = np.array(([1000, 0, 1024], [0, 1001, 775], [0, 0, 1]), dtype=np.float64) + intrinsics = Intrinsics( + fx_px=fx_px, + fy_px=fy_px, + cx_px=cx_px, + cy_px=cy_px, + width_px=width_px, + height_px=height_px, + ) + K_expected: NDArrayFloat = np.array( + ([1000, 0, 1024], [0, 1001, 775], [0, 0, 1]), dtype=np.float64 + ) assert np.array_equal(intrinsics.K, K_expected) @@ -93,7 +113,13 @@ def test_right_clipping_plane() -> None: fx_px = 10.0 width_px = 30 pinhole_camera = _create_pinhole_camera( - fx_px=fx_px, fy_px=0, cx_px=0, cy_px=0, height_px=30, width_px=width_px, cam_name="ring_front_center" + fx_px=fx_px, + fy_px=0, + cx_px=0, + cy_px=0, + height_px=30, + width_px=width_px, + cam_name="ring_front_center", ) right_plane = pinhole_camera.right_clipping_plane @@ -139,7 +165,13 @@ def test_left_clipping_plane() -> None: width_px = 30 pinhole_camera = _create_pinhole_camera( - fx_px=fx_px, fy_px=0, cx_px=0, cy_px=0, height_px=30, width_px=width_px, cam_name="ring_front_center" + fx_px=fx_px, + fy_px=0, + cx_px=0, + cy_px=0, + height_px=30, + width_px=width_px, + cam_name="ring_front_center", ) left_plane = pinhole_camera.left_clipping_plane @@ -181,7 +213,13 @@ def test_top_clipping_plane() -> None: fx_px = 10.0 height_px = 45 pinhole_camera = _create_pinhole_camera( - fx_px=fx_px, fy_px=0, cx_px=0, cy_px=0, height_px=height_px, width_px=1000, cam_name="ring_front_center" + fx_px=fx_px, + fy_px=0, + cx_px=0, + cy_px=0, + height_px=height_px, + width_px=1000, + cam_name="ring_front_center", ) top_plane = pinhole_camera.top_clipping_plane @@ -227,7 +265,13 @@ def test_bottom_clipping_plane() -> None: width_px = 10000 pinhole_camera = _create_pinhole_camera( - fx_px=fx_px, fy_px=1, cx_px=0, cy_px=0, height_px=height_px, width_px=width_px, cam_name="ring_front_center" + fx_px=fx_px, + fy_px=1, + cx_px=0, + cy_px=0, + height_px=height_px, + width_px=width_px, + cam_name="ring_front_center", ) bottom_plane = pinhole_camera.bottom_clipping_plane @@ -258,7 +302,13 @@ def test_form_near_clipping_plane() -> None: near_clip_dist = 30.0 pinhole_camera = _create_pinhole_camera( - fx_px=1, fy_px=0, cx_px=0, cy_px=0, height_px=30, width_px=width_px, cam_name="ring_front_center" + fx_px=1, + fy_px=0, + cx_px=0, + cy_px=0, + height_px=30, + width_px=width_px, + cam_name="ring_front_center", ) near_plane = pinhole_camera.near_clipping_plane(near_clip_dist) @@ -298,9 +348,21 @@ def test_frustum_planes_ring_cam() -> None: width_px = 2048 pinhole_camera = _create_pinhole_camera( - fx_px=fx_px, fy_px=fy_px, cx_px=cx_px, cy_px=cy_px, height_px=height_px, width_px=width_px, cam_name=camera_name + fx_px=fx_px, + fy_px=fy_px, + cx_px=cx_px, + cy_px=cy_px, + height_px=height_px, + width_px=width_px, + cam_name=camera_name, ) - left_plane, right_plane, near_plane, bottom_plane, top_plane = pinhole_camera.frustum_planes(near_clip_dist) + ( + left_plane, + right_plane, + near_plane, + bottom_plane, + top_plane, + ) = pinhole_camera.frustum_planes(near_clip_dist) left_plane_expected: NDArrayFloat = np.array([fx_px, 0.0, width_px / 2.0, 0.0]) right_plane_expected: NDArrayFloat = np.array([-fx_px, 0.0, width_px / 2.0, 0.0]) @@ -308,10 +370,18 @@ def test_frustum_planes_ring_cam() -> None: bottom_plane_expected: NDArrayFloat = np.array([0.0, -fx_px, height_px / 2.0, 0.0]) top_plane_expected: NDArrayFloat = np.array([0.0, fx_px, height_px / 2.0, 0.0]) - assert np.allclose(left_plane, left_plane_expected / np.linalg.norm(left_plane_expected)) # type: ignore - assert np.allclose(right_plane, right_plane_expected / np.linalg.norm(right_plane_expected)) # type: ignore - assert np.allclose(bottom_plane, bottom_plane_expected / np.linalg.norm(bottom_plane_expected)) # type: ignore - assert np.allclose(top_plane, top_plane_expected / np.linalg.norm(top_plane_expected)) # type: ignore + assert np.allclose( + left_plane, left_plane_expected / np.linalg.norm(left_plane_expected) + ) + assert np.allclose( + right_plane, right_plane_expected / np.linalg.norm(right_plane_expected) + ) + assert np.allclose( + bottom_plane, bottom_plane_expected / np.linalg.norm(bottom_plane_expected) + ) + assert np.allclose( + top_plane, top_plane_expected / np.linalg.norm(top_plane_expected) + ) assert np.allclose(near_plane, near_plane_expected) @@ -336,9 +406,21 @@ def test_generate_frustum_planes_stereo() -> None: width_px = 2048 pinhole_camera = _create_pinhole_camera( - fx_px=fx_px, fy_px=fy_px, cx_px=cx_px, cy_px=cy_px, height_px=height_px, width_px=width_px, cam_name=camera_name + fx_px=fx_px, + fy_px=fy_px, + cx_px=cx_px, + cy_px=cy_px, + height_px=height_px, + width_px=width_px, + cam_name=camera_name, ) - left_plane, right_plane, near_plane, bottom_plane, top_plane = pinhole_camera.frustum_planes(near_clip_dist) + ( + left_plane, + right_plane, + near_plane, + bottom_plane, + top_plane, + ) = pinhole_camera.frustum_planes(near_clip_dist) left_plane_expected: NDArrayFloat = np.array([fx_px, 0.0, width_px / 2.0, 0.0]) right_plane_expected: NDArrayFloat = np.array([-fx_px, 0.0, width_px / 2.0, 0.0]) @@ -346,10 +428,18 @@ def test_generate_frustum_planes_stereo() -> None: bottom_plane_expected: NDArrayFloat = np.array([0.0, -fx_px, height_px / 2.0, 0.0]) top_plane_expected: NDArrayFloat = np.array([0.0, fx_px, height_px / 2.0, 0.0]) - assert np.allclose(left_plane, left_plane_expected / np.linalg.norm(left_plane_expected)) # type: ignore - assert np.allclose(right_plane, right_plane_expected / np.linalg.norm(right_plane_expected)) # type: ignore - assert np.allclose(bottom_plane, bottom_plane_expected / np.linalg.norm(bottom_plane_expected)) # type: ignore - assert np.allclose(top_plane, top_plane_expected / np.linalg.norm(top_plane_expected)) # type: ignore + assert np.allclose( + left_plane, left_plane_expected / np.linalg.norm(left_plane_expected) + ) + assert np.allclose( + right_plane, right_plane_expected / np.linalg.norm(right_plane_expected) + ) + assert np.allclose( + bottom_plane, bottom_plane_expected / np.linalg.norm(bottom_plane_expected) + ) + assert np.allclose( + top_plane, top_plane_expected / np.linalg.norm(top_plane_expected) + ) assert np.allclose(near_plane, near_plane_expected) @@ -422,7 +512,7 @@ def test_compute_pixel_ray_directions_vectorized() -> None: ray_dirs = pinhole_camera.compute_pixel_ray_directions(uv) gt_ray_dir: NDArrayFloat = np.array([2, -3, 10.0]) - gt_ray_dir /= np.linalg.norm(gt_ray_dir) # type: ignore + gt_ray_dir /= np.linalg.norm(gt_ray_dir) for i in range(4): assert np.allclose(gt_ray_dir, ray_dirs[i]) @@ -483,12 +573,14 @@ def test_compute_pixel_rays() -> None: ray_dir = _compute_pixel_ray_direction(u, v, fx, fy, img_w, img_h) gt_ray_dir: NDArrayFloat = np.array([2.0, -3.0, 10.0]) - gt_ray_dir /= np.linalg.norm(gt_ray_dir) # type: ignore + gt_ray_dir /= np.linalg.norm(gt_ray_dir) assert np.allclose(gt_ray_dir, ray_dir) -def _compute_pixel_ray_direction(u: float, v: float, fx: float, fy: float, img_w: int, img_h: int) -> NDArrayFloat: +def _compute_pixel_ray_direction( + u: float, v: float, fx: float, fy: float, img_w: int, img_h: int +) -> NDArrayFloat: r"""Generate rays in the camera coordinate frame. Note: only used as a test utility. @@ -523,7 +615,9 @@ def _compute_pixel_ray_direction(u: float, v: float, fx: float, fy: float, img_w ValueError: If horizontal and vertical focal lengths are not close (within 1e-3). """ if not np.isclose(fx, fy, atol=1e-3): - raise ValueError(f"Focal lengths in the x and y directions must match: {fx} != {fy}") + raise ValueError( + f"Focal lengths in the x and y directions must match: {fx} != {fy}" + ) # approximation for principal point px = img_w / 2 @@ -536,7 +630,7 @@ def _compute_pixel_ray_direction(u: float, v: float, fx: float, fy: float, img_w y_center_offs = v - py ray_dir: NDArrayFloat = np.array([x_center_offs, y_center_offs, fx]) - ray_dir /= np.linalg.norm(ray_dir) # type: ignore + ray_dir /= np.linalg.norm(ray_dir) return ray_dir @@ -591,9 +685,13 @@ def test_get_egovehicle_yaw_cam() -> None: for cam_enum in list(RingCameras): cam_name = cam_enum.value - pinhole_camera = PinholeCamera.from_feather(log_dir=sample_log_dir, cam_name=cam_name) + pinhole_camera = PinholeCamera.from_feather( + log_dir=sample_log_dir, cam_name=cam_name + ) ego_yaw_cam_deg = np.rad2deg(pinhole_camera.egovehicle_yaw_cam_rad) - assert np.isclose(ego_yaw_cam_deg, expected_ego_yaw_cam_deg_dict[cam_name], atol=0.1) + assert np.isclose( + ego_yaw_cam_deg, expected_ego_yaw_cam_deg_dict[cam_name], atol=0.1 + ) np.rad2deg(pinhole_camera.fov_theta_rad) diff --git a/tests/geometry/test_utm.py b/tests/unit/geometry/test_utm.py similarity index 89% rename from tests/geometry/test_utm.py rename to tests/unit/geometry/test_utm.py index 45ed9960..eafbfcb3 100644 --- a/tests/geometry/test_utm.py +++ b/tests/unit/geometry/test_utm.py @@ -19,7 +19,9 @@ def test_convert_city_coords_to_wgs84_atx() -> None: ] ) - wgs84_coords = geo_utils.convert_city_coords_to_wgs84(points_city, city_name=CityName.ATX) + wgs84_coords = geo_utils.convert_city_coords_to_wgs84( + points_city, city_name=CityName.ATX + ) expected_wgs84_coords: NDArrayFloat = np.array( [ @@ -40,7 +42,9 @@ def test_convert_city_coords_to_wgs84_wdc() -> None: ] ) - wgs84_coords = geo_utils.convert_city_coords_to_wgs84(points_city, city_name=CityName.WDC) + wgs84_coords = geo_utils.convert_city_coords_to_wgs84( + points_city, city_name=CityName.WDC + ) expected_wgs84_coords: NDArrayFloat = np.array( [ [38.9299801515994, -77.0168603173312], diff --git a/tests/map/test_drivable_area.py b/tests/unit/map/test_drivable_area.py similarity index 88% rename from tests/map/test_drivable_area.py rename to tests/unit/map/test_drivable_area.py index 3d6b4a8c..56deb5d1 100644 --- a/tests/map/test_drivable_area.py +++ b/tests/unit/map/test_drivable_area.py @@ -29,4 +29,6 @@ def test_from_dict(self) -> None: assert isinstance(drivable_area, DrivableArea) assert drivable_area.id == 4499430 - assert len(drivable_area.area_boundary) == 4 # first vertex is repeated as the last vertex + assert ( + len(drivable_area.area_boundary) == 4 + ) # first vertex is repeated as the last vertex diff --git a/tests/map/test_lane_segment.py b/tests/unit/map/test_lane_segment.py similarity index 92% rename from tests/map/test_lane_segment.py rename to tests/unit/map/test_lane_segment.py index 9ee9997c..160b8de1 100644 --- a/tests/map/test_lane_segment.py +++ b/tests/unit/map/test_lane_segment.py @@ -24,7 +24,10 @@ def test_from_dict(self) -> None: ], "left_lane_mark_type": "SOLID_YELLOW", "left_neighbor_id": None, - "right_lane_boundary": [{"x": 874.01, "y": -105.15, "z": -19.58}, {"x": 890.58, "y": -104.26, "z": -19.58}], + "right_lane_boundary": [ + {"x": 874.01, "y": -105.15, "z": -19.58}, + {"x": 890.58, "y": -104.26, "z": -19.58}, + ], "right_lane_mark_type": "SOLID_WHITE", "right_neighbor_id": 93269520, "predecessors": [], diff --git a/tests/map/test_map_api.py b/tests/unit/map/test_map_api.py similarity index 76% rename from tests/map/test_map_api.py rename to tests/unit/map/test_map_api.py index 1e1e16d1..aa07b9e6 100644 --- a/tests/map/test_map_api.py +++ b/tests/unit/map/test_map_api.py @@ -1,7 +1,6 @@ # -""" -Tests for the Argoverse 2.0 map API. +"""Tests for the Argoverse 2 map API. Uses a simplified map with 2 pedestrian crossings, and 3 lane segments. """ @@ -30,7 +29,9 @@ def dummy_static_map(test_data_root_dir: Path) -> ArgoverseStaticMap: Static map instantiated from dummy test data. """ log_map_dirpath = ( - test_data_root_dir / "static_maps" / "dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076" + test_data_root_dir + / "static_maps" + / "dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076" ) return ArgoverseStaticMap.from_map_dir(log_map_dirpath, build_raster=True) @@ -47,7 +48,9 @@ def full_static_map(test_data_root_dir: Path) -> ArgoverseStaticMap: Static map instantiated from full test data. """ log_map_dirpath = ( - test_data_root_dir / "static_maps" / "full_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076" + test_data_root_dir + / "static_maps" + / "full_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076" ) return ArgoverseStaticMap.from_map_dir(log_map_dirpath, build_raster=True) @@ -57,7 +60,10 @@ class TestPolyline: def test_from_list(self) -> None: """Ensure object is generated correctly from a list of dictionaries.""" - points_dict_list = [{"x": 874.01, "y": -105.15, "z": -19.58}, {"x": 890.58, "y": -104.26, "z": -19.58}] + points_dict_list = [ + {"x": 874.01, "y": -105.15, "z": -19.58}, + {"x": 890.58, "y": -104.26, "z": -19.58}, + ] polyline = Polyline.from_json_data(points_dict_list) assert isinstance(polyline, Polyline) @@ -91,8 +97,14 @@ def test_from_dict(self) -> None: """Ensure object is generated correctly from a dictionary.""" json_data = { "id": 6310421, - "edge1": [{"x": 899.17, "y": -91.52, "z": -19.58}, {"x": 915.68, "y": -93.93, "z": -19.53}], - "edge2": [{"x": 899.44, "y": -95.37, "z": -19.48}, {"x": 918.25, "y": -98.05, "z": -19.4}], + "edge1": [ + {"x": 899.17, "y": -91.52, "z": -19.58}, + {"x": 915.68, "y": -93.93, "z": -19.53}, + ], + "edge2": [ + {"x": 899.44, "y": -95.37, "z": -19.48}, + {"x": 918.25, "y": -98.05, "z": -19.4}, + ], } pedestrian_crossing = PedestrianCrossing.from_dict(json_data) @@ -102,7 +114,9 @@ def test_from_dict(self) -> None: class TestArgoverseStaticMap: """Unit test for the Argoverse 2.0 per-log map.""" - def test_get_lane_segment_successor_ids(self, dummy_static_map: ArgoverseStaticMap) -> None: + def test_get_lane_segment_successor_ids( + self, dummy_static_map: ArgoverseStaticMap + ) -> None: """Ensure lane segment successors are fetched properly.""" lane_segment_id = 93269421 successor_ids = dummy_static_map.get_lane_segment_successor_ids(lane_segment_id) @@ -119,7 +133,9 @@ def test_get_lane_segment_successor_ids(self, dummy_static_map: ArgoverseStaticM expected_successor_ids = [93269526] assert successor_ids == expected_successor_ids - def test_lane_is_in_intersection(self, dummy_static_map: ArgoverseStaticMap) -> None: + def test_lane_is_in_intersection( + self, dummy_static_map: ArgoverseStaticMap + ) -> None: """Ensure the attribute describing if a lane segment is located with an intersection is fetched properly.""" lane_segment_id = 93269421 in_intersection = dummy_static_map.lane_is_in_intersection(lane_segment_id) @@ -136,44 +152,64 @@ def test_lane_is_in_intersection(self, dummy_static_map: ArgoverseStaticMap) -> assert isinstance(in_intersection, bool) assert not in_intersection - def test_get_lane_segment_left_neighbor_id(self, dummy_static_map: ArgoverseStaticMap) -> None: + def test_get_lane_segment_left_neighbor_id( + self, dummy_static_map: ArgoverseStaticMap + ) -> None: """Test getting a lane segment id from the left neighbor.""" # Ensure id of lane segment (if any) that is the left neighbor to the query lane segment can be fetched properly lane_segment_id = 93269421 - l_neighbor_id = dummy_static_map.get_lane_segment_left_neighbor_id(lane_segment_id) + l_neighbor_id = dummy_static_map.get_lane_segment_left_neighbor_id( + lane_segment_id + ) assert l_neighbor_id is None lane_segment_id = 93269500 - l_neighbor_id = dummy_static_map.get_lane_segment_left_neighbor_id(lane_segment_id) + l_neighbor_id = dummy_static_map.get_lane_segment_left_neighbor_id( + lane_segment_id + ) assert l_neighbor_id is None lane_segment_id = 93269520 - l_neighbor_id = dummy_static_map.get_lane_segment_left_neighbor_id(lane_segment_id) + l_neighbor_id = dummy_static_map.get_lane_segment_left_neighbor_id( + lane_segment_id + ) assert l_neighbor_id == 93269421 - def test_get_lane_segment_right_neighbor_id(self, dummy_static_map: ArgoverseStaticMap) -> None: + def test_get_lane_segment_right_neighbor_id( + self, dummy_static_map: ArgoverseStaticMap + ) -> None: """Test getting a lane segment id from the right neighbor.""" # Ensure id of lane segment (if any) that is the right neighbor to the query lane segment can be fetched lane_segment_id = 93269421 - r_neighbor_id = dummy_static_map.get_lane_segment_right_neighbor_id(lane_segment_id) + r_neighbor_id = dummy_static_map.get_lane_segment_right_neighbor_id( + lane_segment_id + ) assert r_neighbor_id == 93269520 lane_segment_id = 93269500 - r_neighbor_id = dummy_static_map.get_lane_segment_right_neighbor_id(lane_segment_id) + r_neighbor_id = dummy_static_map.get_lane_segment_right_neighbor_id( + lane_segment_id + ) assert r_neighbor_id == 93269526 lane_segment_id = 93269520 - r_neighbor_id = dummy_static_map.get_lane_segment_right_neighbor_id(lane_segment_id) + r_neighbor_id = dummy_static_map.get_lane_segment_right_neighbor_id( + lane_segment_id + ) assert r_neighbor_id == 93269458 - def test_get_scenario_lane_segment_ids(self, dummy_static_map: ArgoverseStaticMap) -> None: + def test_get_scenario_lane_segment_ids( + self, dummy_static_map: ArgoverseStaticMap + ) -> None: """Ensure ids of all lane segments in the local map can be fetched properly.""" lane_segment_ids = dummy_static_map.get_scenario_lane_segment_ids() expected_lane_segment_ids = [93269421, 93269500, 93269520] assert lane_segment_ids == expected_lane_segment_ids - def test_get_lane_segment_polygon(self, dummy_static_map: ArgoverseStaticMap) -> None: + def test_get_lane_segment_polygon( + self, dummy_static_map: ArgoverseStaticMap + ) -> None: """Ensure lane segment polygons are fetched properly.""" lane_segment_id = 93269421 @@ -190,9 +226,11 @@ def test_get_lane_segment_polygon(self, dummy_static_map: ArgoverseStaticMap) -> [874.01, -105.15, -19.58], ] ) - np.testing.assert_allclose(ls_polygon, expected_ls_polygon) # type: ignore + np.testing.assert_allclose(ls_polygon, expected_ls_polygon) - def test_get_lane_segment_centerline(self, dummy_static_map: ArgoverseStaticMap) -> None: + def test_get_lane_segment_centerline( + self, dummy_static_map: ArgoverseStaticMap + ) -> None: """Ensure lane segment centerlines can be inferred and fetched properly.""" lane_segment_id = 93269421 @@ -213,16 +251,20 @@ def test_get_lane_segment_centerline(self, dummy_static_map: ArgoverseStaticMap) [890.435, -102.41, -19.62], ] ) - np.testing.assert_allclose(centerline, expected_centerline) # type: ignore + np.testing.assert_allclose(centerline, expected_centerline) - def test_get_scenario_lane_segments(self, dummy_static_map: ArgoverseStaticMap) -> None: + def test_get_scenario_lane_segments( + self, dummy_static_map: ArgoverseStaticMap + ) -> None: """Ensure that all LaneSegment objects in the local map can be returned as a list.""" vector_lane_segments = dummy_static_map.get_scenario_lane_segments() assert isinstance(vector_lane_segments, list) assert all([isinstance(vls, LaneSegment) for vls in vector_lane_segments]) assert len(vector_lane_segments) == 3 - def test_get_scenario_ped_crossings(self, dummy_static_map: ArgoverseStaticMap) -> None: + def test_get_scenario_ped_crossings( + self, dummy_static_map: ArgoverseStaticMap + ) -> None: """Ensure that all PedCrossing objects in the local map can be returned as a list.""" ped_crossings = dummy_static_map.get_scenario_ped_crossings() assert isinstance(ped_crossings, list) @@ -261,9 +303,16 @@ def test_get_scenario_ped_crossings(self, dummy_static_map: ArgoverseStaticMap) ] # fmt: on assert len(ped_crossings) == len(expected_ped_crossings) - assert all([pc == expected_pc for pc, expected_pc in zip(ped_crossings, expected_ped_crossings)]) + assert all( + [ + pc == expected_pc + for pc, expected_pc in zip(ped_crossings, expected_ped_crossings) + ] + ) - def test_get_scenario_vector_drivable_areas(self, dummy_static_map: ArgoverseStaticMap) -> None: + def test_get_scenario_vector_drivable_areas( + self, dummy_static_map: ArgoverseStaticMap + ) -> None: """Ensure that drivable areas are loaded and formatted correctly.""" vector_das = dummy_static_map.get_scenario_vector_drivable_areas() assert isinstance(vector_das, list) @@ -275,7 +324,7 @@ def test_get_scenario_vector_drivable_areas(self, dummy_static_map: ArgoverseSta assert vector_da.xyz.shape == (172, 3) # compare first and last vertex, for equality - np.testing.assert_allclose(vector_da.xyz[0], vector_da.xyz[171]) # type: ignore + np.testing.assert_allclose(vector_da.xyz[0], vector_da.xyz[171]) # fmt: off # compare first 4 vertices @@ -285,23 +334,37 @@ def test_get_scenario_vector_drivable_areas(self, dummy_static_map: ArgoverseSta [904.64, -137.25, -19.28], [904.37, -132.55, -19.32]]) # fmt: on - np.testing.assert_allclose(vector_da.xyz[:4], expected_first4_vertices) # type: ignore + np.testing.assert_allclose(vector_da.xyz[:4], expected_first4_vertices) - def test_get_ground_height_at_xy(self, dummy_static_map: ArgoverseStaticMap) -> None: + def test_get_ground_height_at_xy( + self, dummy_static_map: ArgoverseStaticMap + ) -> None: """Ensure that ground height at (x,y) locations can be retrieved properly.""" point_cloud: NDArrayFloat = np.array( [ [770.6398, -105.8351, -19.4105], # ego-vehicle pose at one timestamp [943.5386, -49.6295, -19.3291], # ego-vehicle pose at one timestamp [918.0960, 82.5588, -20.5742], # ego-vehicle pose at one timestamp - [9999999, 999999, 0], # obviously out of bounds value for city coordinate system - [-999999, -999999, 0], # obviously out of bounds value for city coordinate system + [ + 9999999, + 999999, + 0, + ], # obviously out of bounds value for city coordinate system + [ + -999999, + -999999, + 0, + ], # obviously out of bounds value for city coordinate system ] ) assert dummy_static_map.raster_ground_height_layer is not None - ground_height_z = dummy_static_map.raster_ground_height_layer.get_ground_height_at_xy(point_cloud) + ground_height_z = ( + dummy_static_map.raster_ground_height_layer.get_ground_height_at_xy( + point_cloud + ) + ) assert ground_height_z.shape[0] == point_cloud.shape[0] assert ground_height_z.dtype == np.dtype(np.float64) @@ -311,17 +374,29 @@ def test_get_ground_height_at_xy(self, dummy_static_map: ArgoverseStaticMap) -> # based on grid resolution, ground should be within 7 centimeters of 30cm under back axle. expected_ground = point_cloud[:3, 2] - 0.30 - assert np.allclose(np.absolute(expected_ground - ground_height_z[:3]), 0, atol=0.07) + assert np.allclose( + np.absolute(expected_ground - ground_height_z[:3]), 0, atol=0.07 + ) - def test_get_ground_points_boolean(self, dummy_static_map: ArgoverseStaticMap) -> None: + def test_get_ground_points_boolean( + self, dummy_static_map: ArgoverseStaticMap + ) -> None: """Ensure that points close to the ground surface are correctly classified as `ground` category.""" point_cloud: NDArrayFloat = np.array( [ [770.6398, -105.8351, -19.4105], # ego-vehicle pose at one timestamp [943.5386, -49.6295, -19.3291], # ego-vehicle pose at one timestamp [918.0960, 82.5588, -20.5742], # ego-vehicle pose at one timestamp - [9999999, 999999, 0], # obviously out of bounds value for city coordinate system - [-999999, -999999, 0], # obviously out of bounds value for city coordinate system + [ + 9999999, + 999999, + 0, + ], # obviously out of bounds value for city coordinate system + [ + -999999, + -999999, + 0, + ], # obviously out of bounds value for city coordinate system ] ) @@ -331,7 +406,11 @@ def test_get_ground_points_boolean(self, dummy_static_map: ArgoverseStaticMap) - assert dummy_static_map.raster_ground_height_layer is not None - is_ground_pt = dummy_static_map.raster_ground_height_layer.get_ground_points_boolean(point_cloud) + is_ground_pt = ( + dummy_static_map.raster_ground_height_layer.get_ground_points_boolean( + point_cloud + ) + ) expected_is_ground_pt: NDArrayBool = np.array([True, True, True, False, False]) assert is_ground_pt.dtype == np.dtype(bool) assert np.array_equal(is_ground_pt, expected_is_ground_pt) @@ -341,7 +420,10 @@ def test_load_motion_forecasting_map(test_data_root_dir: Path) -> None: """Try to load a real map from the motion forecasting dataset.""" mf_scenario_id = "0a1e6f0a-1817-4a98-b02e-db8c9327d151" mf_scenario_map_path = ( - test_data_root_dir / "forecasting_scenarios" / mf_scenario_id / f"log_map_archive_{mf_scenario_id}.json" + test_data_root_dir + / "forecasting_scenarios" + / mf_scenario_id + / f"log_map_archive_{mf_scenario_id}.json" ) mf_map = ArgoverseStaticMap.from_json(mf_scenario_map_path) diff --git a/tests/rendering/ops/test_draw.py b/tests/unit/rendering/ops/test_draw.py similarity index 81% rename from tests/rendering/ops/test_draw.py rename to tests/unit/rendering/ops/test_draw.py index 0feb5a05..5b50ef91 100644 --- a/tests/rendering/ops/test_draw.py +++ b/tests/unit/rendering/ops/test_draw.py @@ -7,12 +7,20 @@ import cv2 import numpy as np -from av2.rendering.ops.draw import alpha_blend_kernel, draw_points_kernel, gaussian_kernel +from av2.rendering.ops.draw import ( + alpha_blend_kernel, + draw_points_kernel, + gaussian_kernel, +) from av2.utils.typing import NDArrayByte, NDArrayInt def _draw_points_cv2( - img: NDArrayByte, points_xy: NDArrayInt, colors: NDArrayByte, radius: int, with_anti_alias: bool = True + img: NDArrayByte, + points_xy: NDArrayInt, + colors: NDArrayByte, + radius: int, + with_anti_alias: bool = True, ) -> NDArrayByte: """Draw points in an image using OpenCV functionality. @@ -81,7 +89,12 @@ def test_draw_points_kernel_3x3_antialiased() -> None: dtype=np.uint8, ) img = draw_points_kernel( - img=img, points_uv=points_xy, colors=colors, diameter=diameter, sigma=sigma, with_anti_alias=True + img=img, + points_uv=points_xy, + colors=colors, + diameter=diameter, + sigma=sigma, + with_anti_alias=True, ) assert np.array_equal(img, expected_img) @@ -105,7 +118,12 @@ def test_draw_points_kernel_9x9_aliased() -> None: dtype=np.uint8, ) img = draw_points_kernel( - img=img, points_uv=points_xy, colors=colors, diameter=diameter, sigma=sigma, with_anti_alias=False + img=img, + points_uv=points_xy, + colors=colors, + diameter=diameter, + sigma=sigma, + with_anti_alias=False, ) assert np.array_equal(img, expected_img) @@ -113,26 +131,42 @@ def test_draw_points_kernel_9x9_aliased() -> None: def test_benchmark_draw_points_kernel_aliased(benchmark: Callable[..., Any]) -> None: """Benchmark the draw points kernel _without_ anti-aliasing.""" img: NDArrayByte = np.zeros((2048, 2048, 3), dtype=np.uint8) - points_xy: NDArrayInt = np.random.randint(low=0, high=2048, size=(60000, 2)) - colors: NDArrayByte = np.random.randint(low=0, high=255, size=(60000, 3)).astype(np.uint8) + points_xy: NDArrayInt = np.random.randint(low=0, high=2048, size=(60000, 2)).astype( + np.int64 + ) + colors: NDArrayByte = np.random.randint(low=0, high=255, size=(60000, 3)).astype( + np.uint8 + ) diameter = 10 benchmark(draw_points_kernel, img, points_xy, colors, diameter) -def test_benchmark_draw_points_kernel_anti_aliased(benchmark: Callable[..., Any]) -> None: +def test_benchmark_draw_points_kernel_anti_aliased( + benchmark: Callable[..., Any] +) -> None: """Benchmark the draw points kernel _with_ anti-aliasing.""" img: NDArrayByte = np.zeros((2048, 2048, 3), dtype=np.uint8) - points_xy: NDArrayInt = np.random.randint(low=0, high=2048, size=(60000, 2)) - colors: NDArrayByte = np.random.randint(low=0, high=255, size=(60000, 3)).astype(np.uint8) + points_xy: NDArrayInt = np.random.randint(low=0, high=2048, size=(60000, 2)).astype( + np.int64 + ) + colors: NDArrayByte = np.random.randint(low=0, high=255, size=(60000, 3)).astype( + np.uint8 + ) diameter = 10 - benchmark(draw_points_kernel, img, points_xy, colors, diameter, with_anti_alias=True) + benchmark( + draw_points_kernel, img, points_xy, colors, diameter, with_anti_alias=True + ) def test_benchmark_draw_points_cv2_aliased(benchmark: Callable[..., Any]) -> None: """Benchmark the draw points method from OpenCV _without_ anti-aliasing.""" img: NDArrayByte = np.zeros((2048, 2048, 3), dtype=np.uint8) - points_xy: NDArrayInt = np.random.randint(low=0, high=2048, size=(60000, 2)) - colors: NDArrayByte = np.random.randint(low=0, high=255, size=(60000, 3)).astype(np.uint8) + points_xy: NDArrayInt = np.random.randint(low=0, high=2048, size=(60000, 2)).astype( + np.int64 + ) + colors: NDArrayByte = np.random.randint(low=0, high=255, size=(60000, 3)).astype( + np.uint8 + ) radius = 10 benchmark(_draw_points_cv2, img, points_xy, colors, radius, with_anti_alias=False) @@ -140,7 +174,11 @@ def test_benchmark_draw_points_cv2_aliased(benchmark: Callable[..., Any]) -> Non def test_benchmark_draw_points_cv2_anti_aliased(benchmark: Callable[..., Any]) -> None: """Benchmark the draw points method from OpenCV _with_ anti-aliasing.""" img: NDArrayByte = np.zeros((2048, 2048, 3), dtype=np.uint8) - points_xy: NDArrayInt = np.random.randint(low=0, high=2048, size=(60000, 2)) - colors: NDArrayByte = np.random.randint(low=0, high=255, size=(60000, 3)).astype(np.uint8) + points_xy: NDArrayInt = np.random.randint(low=0, high=2048, size=(60000, 2)).astype( + np.int64 + ) + colors: NDArrayByte = np.random.randint(low=0, high=255, size=(60000, 3)).astype( + np.uint8 + ) radius = 10 benchmark(_draw_points_cv2, img, points_xy, colors, radius, with_anti_alias=True) diff --git a/tests/rendering/test_color.py b/tests/unit/rendering/test_color.py similarity index 80% rename from tests/rendering/test_color.py rename to tests/unit/rendering/test_color.py index c30d07b5..0e22ce2e 100644 --- a/tests/rendering/test_color.py +++ b/tests/unit/rendering/test_color.py @@ -10,7 +10,9 @@ def test_create_colormap() -> None: """Ensure we can create a red-to-green RGB colormap with values in [0,1].""" - colors_arr_rgb = color_utils.create_colormap(color_list=[RED_HEX, GREEN_HEX], n_colors=10) + colors_arr_rgb = color_utils.create_colormap( + color_list=[RED_HEX, GREEN_HEX], n_colors=10 + ) assert np.logical_and(0 <= colors_arr_rgb, colors_arr_rgb <= 1).all() assert colors_arr_rgb.shape == (10, 3) diff --git a/tests/rendering/test_map.py b/tests/unit/rendering/test_map.py similarity index 87% rename from tests/rendering/test_map.py rename to tests/unit/rendering/test_map.py index 7d8735ae..2abbe275 100644 --- a/tests/rendering/test_map.py +++ b/tests/unit/rendering/test_map.py @@ -14,7 +14,9 @@ def test_draw_visible_polyline_segments_cv2_some_visible() -> None: """Test rendering when one vertex is marked as occluded, and so two line segments are dropped out.""" visualize = False # 6 vertices in the polyline. - line_segments_arr: NDArrayInt = np.array([[50, 0], [50, 20], [50, 40], [50, 60], [50, 80], [60, 120]]) + line_segments_arr: NDArrayInt = np.array( + [[50, 0], [50, 20], [50, 40], [50, 60], [50, 80], [60, 120]] + ) valid_pts_bool: NDArrayBool = np.array([True, True, True, False, True, True]) img_bgr: NDArrayByte = np.zeros((100, 100, 3), dtype=np.uint8) @@ -34,7 +36,9 @@ def test_draw_visible_polyline_segments_cv2_all_visible() -> None: """Test rendering when all vertices are visible (and thus all line segments are visible).""" visualize = False # 6 vertices in the polyline. - line_segments_arr: NDArrayInt = np.array([[50, 0], [50, 20], [50, 40], [50, 60], [50, 80], [60, 120]]) + line_segments_arr: NDArrayInt = np.array( + [[50, 0], [50, 20], [50, 40], [50, 60], [50, 80], [60, 120]] + ) valid_pts_bool: NDArrayBool = np.array([True, True, True, True, True, True]) img_bgr: NDArrayByte = np.zeros((100, 100, 3), dtype=np.uint8) diff --git a/tests/unit/rendering/test_rasterize.py b/tests/unit/rendering/test_rasterize.py new file mode 100644 index 00000000..1830466f --- /dev/null +++ b/tests/unit/rendering/test_rasterize.py @@ -0,0 +1,43 @@ +"""Tests for the rasterize sub-module.""" + +from typing import Tuple + +import numpy as np + +from av2.rendering.rasterize import xyz_to_bev +from av2.utils.typing import NDArrayFloat + + +def _build_dummy_raster_inputs( + n: int, d: int +) -> Tuple[ + NDArrayFloat, Tuple[float, float, float], Tuple[float, float, float], NDArrayFloat +]: + """Build dummy inputs for the rasterize function. + + Args: + n: Number of points. + d: Number of dimensions. + + Returns: + (n,d) points, (3,) voxel resolution, (3,) grid size, (n,) cmap values. + """ + xyz = np.ones((n, d)) + voxel_resolution = (0.1, 0.1, 0.1) + grid_size_m = (50.0, 50.0, 10.0) + cmap = np.ones_like(xyz[:, 0:1]) + return xyz, voxel_resolution, grid_size_m, cmap + + +def test_rasterize_Nx3() -> None: + """Test the rasterize function with (N,3) input.""" + n, d = 1000, 3 + xyz, voxel_resolution, grid_size_m, cmap = _build_dummy_raster_inputs(n, d) + xyz_to_bev(xyz, voxel_resolution, grid_size_m, cmap) + + +def test_rasterize_Nx4() -> None: + """Test the rasterize function with (N,4) input.""" + n, d = 1000, 4 + xyz, voxel_resolution, grid_size_m, cmap = _build_dummy_raster_inputs(n, d) + xyz_to_bev(xyz, voxel_resolution, grid_size_m, cmap) diff --git a/tests/rendering/test_video.py b/tests/unit/rendering/test_video.py similarity index 96% rename from tests/rendering/test_video.py rename to tests/unit/rendering/test_video.py index 18947f70..f76ec0f3 100644 --- a/tests/rendering/test_video.py +++ b/tests/unit/rendering/test_video.py @@ -87,5 +87,7 @@ def test_crop_video_to_even_dims() -> None: save_fpath = Path(NamedTemporaryFile(suffix=".mp4").name) assert not save_fpath.exists() - video_utils.write_video(video=cropped_video, dst=save_fpath, fps=10, preset="medium") + video_utils.write_video( + video=cropped_video, dst=save_fpath, fps=10, preset="medium" + ) assert save_fpath.exists() diff --git a/tests/structures/test_cuboid.py b/tests/unit/structures/test_cuboid.py similarity index 90% rename from tests/structures/test_cuboid.py rename to tests/unit/structures/test_cuboid.py index 1eee301e..f0843080 100644 --- a/tests/structures/test_cuboid.py +++ b/tests/unit/structures/test_cuboid.py @@ -63,7 +63,9 @@ def test_compute_interior_points() -> None: ], dtype=float ) # fmt: on - expected_is_interior: NDArrayBool = np.array([False, True, True, True, True, True, False]) + expected_is_interior: NDArrayBool = np.array( + [False, True, True, True, True, True, False] + ) dst_SE3_object = SE3(rotation=np.eye(3), translation=np.array([2, 0, 0])) @@ -108,7 +110,14 @@ def _get_dummy_cuboid_list_params(num_cuboids: int) -> List[Cuboid]: """Create a cuboid list of length `num_cuboids`.""" cuboids: List[Cuboid] = [] for i in range(num_cuboids): - ego_SE3_object, length_m, width_m, height_m, category, timestamp_ns = _get_dummy_cuboid_params() + ( + ego_SE3_object, + length_m, + width_m, + height_m, + category, + timestamp_ns, + ) = _get_dummy_cuboid_params() cuboid = Cuboid( dst_SE3_object=ego_SE3_object, length_m=length_m + i, @@ -123,7 +132,14 @@ def _get_dummy_cuboid_list_params(num_cuboids: int) -> List[Cuboid]: def test_cuboid_constructor() -> None: """Test initializing a single cuboid.""" - ego_SE3_object, length_m, width_m, height_m, category, timestamp_ns = _get_dummy_cuboid_params() + ( + ego_SE3_object, + length_m, + width_m, + height_m, + category, + timestamp_ns, + ) = _get_dummy_cuboid_params() cuboid = Cuboid( dst_SE3_object=ego_SE3_object, length_m=length_m, @@ -172,13 +188,13 @@ def test_getitem() -> None: assert isinstance(cuboid_list[5], Cuboid) - with pytest.raises(IndexError) as e_info: + with pytest.raises(IndexError): cuboid_list[-1] - with pytest.raises(IndexError) as e_info: + with pytest.raises(IndexError): cuboid_list[50] - with pytest.raises(IndexError) as e_info: + with pytest.raises(IndexError): cuboid_list[51] @@ -229,7 +245,9 @@ def benchmark_transform(cuboids: List[Cuboid], target_SE3_ego: SE3) -> List[Cubo def test_benchmark_transform_list_comprehension(benchmark: Callable[..., Any]) -> None: """Benchmark cuboid transform with list comprehension.""" - def benchmark_transform_list_comprehension(cuboids: List[Cuboid], target_SE3_ego: SE3) -> List[Cuboid]: + def benchmark_transform_list_comprehension( + cuboids: List[Cuboid], target_SE3_ego: SE3 + ) -> List[Cuboid]: transformed_cuboids: List[Cuboid] = [ Cuboid( dst_SE3_object=target_SE3_ego.compose(cuboid.dst_SE3_object), @@ -245,7 +263,9 @@ def benchmark_transform_list_comprehension(cuboids: List[Cuboid], target_SE3_ego num_cuboids = 1000 cuboids = _get_dummy_cuboid_list_params(num_cuboids) - benchmark(benchmark_transform_list_comprehension, cuboids, cuboids[0].dst_SE3_object) + benchmark( + benchmark_transform_list_comprehension, cuboids, cuboids[0].dst_SE3_object + ) if __name__ == "__main__": diff --git a/tests/structures/test_ndgrid.py b/tests/unit/structures/test_ndgrid.py similarity index 95% rename from tests/structures/test_ndgrid.py rename to tests/unit/structures/test_ndgrid.py index 97068142..d32aa5da 100644 --- a/tests/structures/test_ndgrid.py +++ b/tests/unit/structures/test_ndgrid.py @@ -52,7 +52,10 @@ def test_bev_grid() -> None: grid_coordinates_expected: NDArrayInt = np.array([61, 72], dtype=int) values_expected = GRAY_BGR - assert np.array_equal(bev_img[grid_coordinates_expected[1], grid_coordinates_expected[0]], values_expected) + assert np.array_equal( + bev_img[grid_coordinates_expected[1], grid_coordinates_expected[0]], + values_expected, + ) def test_BEVGrid_non_integer_multiple() -> None: diff --git a/tests/structures/test_sweep.py b/tests/unit/structures/test_sweep.py similarity index 78% rename from tests/structures/test_sweep.py rename to tests/unit/structures/test_sweep.py index ebb1c3ce..3b5fb42a 100644 --- a/tests/structures/test_sweep.py +++ b/tests/unit/structures/test_sweep.py @@ -14,13 +14,22 @@ @pytest.fixture def dummy_sweep(test_data_root_dir: Path) -> Sweep: """Get a fake sweep containing two points.""" - path = test_data_root_dir / "sensor_dataset_logs" / "dummy" / "sensors" / "lidar" / "315968663259918000.feather" + path = ( + test_data_root_dir + / "sensor_dataset_logs" + / "dummy" + / "sensors" + / "lidar" + / "315968663259918000.feather" + ) return Sweep.from_feather(path) def test_sweep_from_feather(dummy_sweep: Sweep) -> None: """Test loading a sweep from a feather file.""" - xyz_expected: NDArrayFloat = np.array([[-22.1875, 20.484375, 0.55029296875], [-20.609375, 19.1875, 1.30078125]]) + xyz_expected: NDArrayFloat = np.array( + [[-22.1875, 20.484375, 0.55029296875], [-20.609375, 19.1875, 1.30078125]] + ) intensity_expected: NDArrayByte = np.array([38, 5], dtype=np.uint8) laser_number_expected: NDArrayByte = np.array([19, 3], dtype=np.uint8) offset_ns_expected: NDArrayInt = np.array([253440, 283392], dtype=np.int32) diff --git a/tests/test_data/a_Sim2_b.json b/tests/unit/test_data/a_Sim2_b.json similarity index 100% rename from tests/test_data/a_Sim2_b.json rename to tests/unit/test_data/a_Sim2_b.json diff --git a/tests/test_data/a_Sim2_b___invalid.json b/tests/unit/test_data/a_Sim2_b___invalid.json similarity index 100% rename from tests/test_data/a_Sim2_b___invalid.json rename to tests/unit/test_data/a_Sim2_b___invalid.json diff --git a/tests/test_data/forecasting_scenarios/0a1e6f0a-1817-4a98-b02e-db8c9327d151/log_map_archive_0a1e6f0a-1817-4a98-b02e-db8c9327d151.json b/tests/unit/test_data/forecasting_scenarios/0a1e6f0a-1817-4a98-b02e-db8c9327d151/log_map_archive_0a1e6f0a-1817-4a98-b02e-db8c9327d151.json similarity index 100% rename from tests/test_data/forecasting_scenarios/0a1e6f0a-1817-4a98-b02e-db8c9327d151/log_map_archive_0a1e6f0a-1817-4a98-b02e-db8c9327d151.json rename to tests/unit/test_data/forecasting_scenarios/0a1e6f0a-1817-4a98-b02e-db8c9327d151/log_map_archive_0a1e6f0a-1817-4a98-b02e-db8c9327d151.json diff --git a/tests/test_data/forecasting_scenarios/0a1e6f0a-1817-4a98-b02e-db8c9327d151/scenario_0a1e6f0a-1817-4a98-b02e-db8c9327d151.parquet b/tests/unit/test_data/forecasting_scenarios/0a1e6f0a-1817-4a98-b02e-db8c9327d151/scenario_0a1e6f0a-1817-4a98-b02e-db8c9327d151.parquet similarity index 100% rename from tests/test_data/forecasting_scenarios/0a1e6f0a-1817-4a98-b02e-db8c9327d151/scenario_0a1e6f0a-1817-4a98-b02e-db8c9327d151.parquet rename to tests/unit/test_data/forecasting_scenarios/0a1e6f0a-1817-4a98-b02e-db8c9327d151/scenario_0a1e6f0a-1817-4a98-b02e-db8c9327d151.parquet diff --git a/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/calibration/egovehicle_SE3_sensor.feather b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/calibration/egovehicle_SE3_sensor.feather new file mode 100644 index 00000000..94096a15 Binary files /dev/null and b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/calibration/egovehicle_SE3_sensor.feather differ diff --git a/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/calibration/intrinsics.feather b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/calibration/intrinsics.feather new file mode 100644 index 00000000..298b7572 Binary files /dev/null and b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/calibration/intrinsics.feather differ diff --git a/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/city_SE3_egovehicle.feather b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/city_SE3_egovehicle.feather new file mode 100644 index 00000000..5695387e Binary files /dev/null and b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/city_SE3_egovehicle.feather differ diff --git a/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/7fab2350-7eaf-3b7e-a39d-6937a4c1bede___img_Sim2_city.json b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/7fab2350-7eaf-3b7e-a39d-6937a4c1bede___img_Sim2_city.json new file mode 100644 index 00000000..e29cc7e7 --- /dev/null +++ b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/7fab2350-7eaf-3b7e-a39d-6937a4c1bede___img_Sim2_city.json @@ -0,0 +1 @@ +{"R": [1.0, 0.0, 0.0, 1.0], "t": [-5072.400390625, -2283.900390625], "s": 3.3333333333333335} \ No newline at end of file diff --git a/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/7fab2350-7eaf-3b7e-a39d-6937a4c1bede_ground_height_surface____PIT.npy b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/7fab2350-7eaf-3b7e-a39d-6937a4c1bede_ground_height_surface____PIT.npy new file mode 100644 index 00000000..c166fdfb Binary files /dev/null and b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/7fab2350-7eaf-3b7e-a39d-6937a4c1bede_ground_height_surface____PIT.npy differ diff --git a/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/log_map_archive_7fab2350-7eaf-3b7e-a39d-6937a4c1bede____PIT_city_47896.json b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/log_map_archive_7fab2350-7eaf-3b7e-a39d-6937a4c1bede____PIT_city_47896.json new file mode 100644 index 00000000..c93311c5 --- /dev/null +++ b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/log_map_archive_7fab2350-7eaf-3b7e-a39d-6937a4c1bede____PIT_city_47896.json @@ -0,0 +1 @@ +{"pedestrian_crossings": {"2356431": {"edge1": [{"x": 5236.97, "y": 2364.34, "z": 69.5}, {"x": 5232.12, "y": 2367.74, "z": 69.33}], "edge2": [{"x": 5239.78, "y": 2365.57, "z": 69.48}, {"x": 5231.75, "y": 2371.19, "z": 69.24}], "id": 2356431}, "2356430": {"edge1": [{"x": 5231.16, "y": 2371.82, "z": 69.22}, {"x": 5231.28, "y": 2389.37, "z": 68.83}], "edge2": [{"x": 5232.5, "y": 2368.79, "z": 69.31}, {"x": 5234.11, "y": 2389.29, "z": 68.86}], "id": 2356430}, "2356429": {"edge1": [{"x": 5241.69, "y": 2382.09, "z": 69.24}, {"x": 5231.72, "y": 2389.22, "z": 68.84}], "edge2": [{"x": 5241.34, "y": 2386.21, "z": 69.15}, {"x": 5235.41, "y": 2390.58, "z": 68.91}], "id": 2356429}, "2356428": {"edge1": [{"x": 5237.61, "y": 2364.89, "z": 69.46}, {"x": 5240.52, "y": 2384.47, "z": 69.12}], "edge2": [{"x": 5240.88, "y": 2364.97, "z": 69.51}, {"x": 5241.81, "y": 2382.09, "z": 69.25}], "id": 2356428}, "2356005": {"edge1": [{"x": 5158.06, "y": 2443.44, "z": 65.63}, {"x": 5146.42, "y": 2427.11, "z": 65.6}], "edge2": [{"x": 5154.27, "y": 2442.85, "z": 65.53}, {"x": 5145.38, "y": 2430.3, "z": 65.57}], "id": 2356005}, "2356004": {"edge1": [{"x": 5152.93, "y": 2421.62, "z": 65.81}, {"x": 5146.07, "y": 2426.42, "z": 65.6}], "edge2": [{"x": 5156.76, "y": 2422.34, "z": 66.01}, {"x": 5145.53, "y": 2430.29, "z": 65.57}], "id": 2356004}, "2356003": {"edge1": [{"x": 5165.63, "y": 2434.47, "z": 66.03}, {"x": 5156.6, "y": 2422.53, "z": 65.99}], "edge2": [{"x": 5165.03, "y": 2438.24, "z": 65.96}, {"x": 5152.83, "y": 2421.66, "z": 65.79}], "id": 2356003}, "2356002": {"edge1": [{"x": 5165.18, "y": 2438.24, "z": 65.97}, {"x": 5158.76, "y": 2443.99, "z": 65.67}], "edge2": [{"x": 5165.8, "y": 2434.43, "z": 66.04}, {"x": 5154.46, "y": 2442.76, "z": 65.53}], "id": 2356002}, "2355546": {"edge1": [{"x": 5319.6, "y": 2331.85, "z": 71.88}, {"x": 5314.36, "y": 2314.22, "z": 72.09}], "edge2": [{"x": 5316.65, "y": 2332.14, "z": 71.89}, {"x": 5311.81, "y": 2315.6, "z": 72.02}], "id": 2355546}, "2355541": {"edge1": [{"x": 5316.13, "y": 2331.42, "z": 71.86}, {"x": 5327.01, "y": 2323.93, "z": 72.02}], "edge2": [{"x": 5319.37, "y": 2333.04, "z": 71.97}, {"x": 5327.8, "y": 2327.46, "z": 72.07}], "id": 2355541}, "2356225": {"edge1": [{"x": 5079.03, "y": 2471.46, "z": 62.84}, {"x": 5090.62, "y": 2463.72, "z": 63.2}], "edge2": [{"x": 5082.22, "y": 2472.42, "z": 62.87}, {"x": 5096.85, "y": 2462.84, "z": 63.44}], "id": 2356225}}, "lane_segments": {"38109167": {"id": 38109167, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5272.94, "y": 2353.69, "z": 70.51}, {"x": 5286.78, "y": 2342.58, "z": 71.04}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5268.73, "y": 2346.16, "z": 70.46}, {"x": 5285.11, "y": 2340.16, "z": 71.03}], "right_lane_mark_type": "NONE", "successors": [38109400], "predecessors": [38117100], "right_neighbor_id": null, "left_neighbor_id": 38109519}, "38109176": {"id": 38109176, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5343.55, "y": 2359.71, "z": 71.69}, {"x": 5327.53, "y": 2329.17, "z": 72.01}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5336.53, "y": 2363.4, "z": 71.7}, {"x": 5323.36, "y": 2338.28, "z": 71.9}, {"x": 5320.76, "y": 2333.27, "z": 71.87}], "right_lane_mark_type": "NONE", "successors": [38111935, 38109260, 38109748, 38109698, 38111936], "predecessors": [38109432, 38109421], "right_neighbor_id": null, "left_neighbor_id": null}, "38109234": {"id": 38109234, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5304.84, "y": 2330.0, "z": 71.66}, {"x": 5286.78, "y": 2342.58, "z": 71.04}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5306.47, "y": 2332.86, "z": 71.62}, {"x": 5288.86, "y": 2345.49, "z": 70.99}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38109519, 38111601], "predecessors": [38111133], "right_neighbor_id": 38111904, "left_neighbor_id": 38109400}, "38109262": {"id": 38109262, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5280.0, "y": 2413.56, "z": 70.11}, {"x": 5296.66, "y": 2402.37, "z": 70.48}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5280.0, "y": 2407.51, "z": 70.05}, {"x": 5290.94, "y": 2400.19, "z": 70.32}, {"x": 5293.79, "y": 2398.19, "z": 70.4}], "right_lane_mark_type": "NONE", "successors": [38116557, 38116487], "predecessors": [38114654], "right_neighbor_id": null, "left_neighbor_id": null}, "38109290": {"id": 38109290, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5310.26, "y": 2326.29, "z": 71.83}, {"x": 5330.21, "y": 2312.7, "z": 72.05}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5308.45, "y": 2323.82, "z": 71.8}, {"x": 5328.5, "y": 2310.17, "z": 72.07}], "right_lane_mark_type": "NONE", "successors": [38109324], "predecessors": [38111103], "right_neighbor_id": 38111849, "left_neighbor_id": 38109397}, "38109317": {"id": 38109317, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5310.26, "y": 2326.29, "z": 71.83}, {"x": 5311.05, "y": 2325.8, "z": 71.85}, {"x": 5311.86, "y": 2325.3, "z": 71.88}, {"x": 5312.67, "y": 2324.78, "z": 71.9}, {"x": 5313.49, "y": 2324.24, "z": 71.92}, {"x": 5314.31, "y": 2323.69, "z": 71.94}, {"x": 5315.13, "y": 2323.13, "z": 71.95}, {"x": 5315.93, "y": 2322.54, "z": 71.97}, {"x": 5316.72, "y": 2321.94, "z": 71.98}, {"x": 5317.49, "y": 2321.32, "z": 71.99}, {"x": 5318.23, "y": 2320.69, "z": 72.01}, {"x": 5318.93, "y": 2320.03, "z": 72.02}, {"x": 5319.6, "y": 2319.36, "z": 72.02}, {"x": 5320.22, "y": 2318.66, "z": 72.02}, {"x": 5320.8, "y": 2317.94, "z": 72.03}, {"x": 5321.32, "y": 2317.21, "z": 72.03}, {"x": 5321.78, "y": 2316.45, "z": 72.04}, {"x": 5322.18, "y": 2315.67, "z": 72.05}, {"x": 5322.5, "y": 2314.87, "z": 72.05}, {"x": 5322.75, "y": 2314.05, "z": 72.06}, {"x": 5322.92, "y": 2313.2, "z": 72.08}, {"x": 5323.0, "y": 2312.33, "z": 72.1}, {"x": 5322.99, "y": 2311.43, "z": 72.12}, {"x": 5322.88, "y": 2310.52, "z": 72.13}, {"x": 5322.67, "y": 2309.57, "z": 72.14}, {"x": 5322.35, "y": 2308.6, "z": 72.15}, {"x": 5321.92, "y": 2307.61, "z": 72.19}, {"x": 5321.42, "y": 2306.55, "z": 72.28}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5308.45, "y": 2323.82, "z": 71.8}, {"x": 5309.21, "y": 2323.24, "z": 71.82}, {"x": 5309.84, "y": 2322.68, "z": 71.84}, {"x": 5310.59, "y": 2322.1, "z": 71.86}, {"x": 5311.37, "y": 2321.46, "z": 71.88}, {"x": 5312.18, "y": 2320.78, "z": 71.9}, {"x": 5312.99, "y": 2320.06, "z": 71.93}, {"x": 5313.78, "y": 2319.3, "z": 71.95}, {"x": 5314.54, "y": 2318.52, "z": 71.97}, {"x": 5315.23, "y": 2317.71, "z": 71.99}, {"x": 5315.85, "y": 2316.9, "z": 72.0}, {"x": 5316.36, "y": 2316.08, "z": 72.02}, {"x": 5316.76, "y": 2315.26, "z": 72.03}, {"x": 5317.01, "y": 2314.45, "z": 72.04}, {"x": 5317.1, "y": 2313.65, "z": 72.05}, {"x": 5317.01, "y": 2312.88, "z": 72.06}, {"x": 5316.72, "y": 2312.13, "z": 72.11}, {"x": 5316.2, "y": 2311.43, "z": 72.15}, {"x": 5315.57, "y": 2310.62, "z": 72.21}], "right_lane_mark_type": "NONE", "successors": [38109302], "predecessors": [38111103], "right_neighbor_id": null, "left_neighbor_id": null}, "38109359": {"id": 38109359, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5244.11, "y": 2370.36, "z": 69.6}, {"x": 5264.5, "y": 2359.3, "z": 70.24}, {"x": 5264.72, "y": 2359.16, "z": 70.25}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5240.78, "y": 2365.26, "z": 69.5}, {"x": 5250.55, "y": 2358.63, "z": 69.81}, {"x": 5260.02, "y": 2352.14, "z": 70.15}], "right_lane_mark_type": "NONE", "successors": [38117100], "predecessors": [38114446, 38114374], "right_neighbor_id": null, "left_neighbor_id": null}, "38109382": {"id": 38109382, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5272.94, "y": 2353.69, "z": 70.51}, {"x": 5264.72, "y": 2359.16, "z": 70.25}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5275.94, "y": 2358.54, "z": 70.49}, {"x": 5268.05, "y": 2363.91, "z": 70.19}], "right_lane_mark_type": "NONE", "successors": [38116085], "predecessors": [38111894, 38109519], "right_neighbor_id": null, "left_neighbor_id": 38117100}, "38109397": {"id": 38109397, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5330.21, "y": 2312.7, "z": 72.05}, {"x": 5310.26, "y": 2326.29, "z": 71.83}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5332.07, "y": 2315.53, "z": 72.0}, {"x": 5312.04, "y": 2329.04, "z": 71.8}], "right_lane_mark_type": "NONE", "successors": [38111133], "predecessors": [38109434], "right_neighbor_id": 38111902, "left_neighbor_id": 38109290}, "38109400": {"id": 38109400, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5286.78, "y": 2342.58, "z": 71.04}, {"x": 5304.84, "y": 2330.0, "z": 71.66}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5285.11, "y": 2340.16, "z": 71.03}, {"x": 5303.12, "y": 2327.48, "z": 71.65}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111103], "predecessors": [38109167], "right_neighbor_id": 38111866, "left_neighbor_id": 38109234}, "38109440": {"id": 38109440, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5272.94, "y": 2353.69, "z": 70.51}, {"x": 5275.78, "y": 2351.57, "z": 70.61}, {"x": 5276.51, "y": 2350.94, "z": 70.64}, {"x": 5277.22, "y": 2350.28, "z": 70.67}, {"x": 5277.92, "y": 2349.58, "z": 70.7}, {"x": 5278.59, "y": 2348.85, "z": 70.73}, {"x": 5279.21, "y": 2348.1, "z": 70.76}, {"x": 5279.78, "y": 2347.32, "z": 70.79}, {"x": 5280.29, "y": 2346.52, "z": 70.82}, {"x": 5280.73, "y": 2345.71, "z": 70.84}, {"x": 5281.09, "y": 2344.88, "z": 70.86}, {"x": 5281.36, "y": 2344.03, "z": 70.88}, {"x": 5281.53, "y": 2343.18, "z": 70.9}, {"x": 5281.58, "y": 2342.32, "z": 70.91}, {"x": 5281.51, "y": 2341.45, "z": 70.92}, {"x": 5281.32, "y": 2340.59, "z": 70.92}, {"x": 5280.98, "y": 2339.72, "z": 70.91}, {"x": 5280.49, "y": 2338.86, "z": 70.88}, {"x": 5279.45, "y": 2337.18, "z": 70.9}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5268.73, "y": 2346.16, "z": 70.46}, {"x": 5268.88, "y": 2346.0, "z": 70.46}, {"x": 5269.41, "y": 2345.65, "z": 70.49}, {"x": 5274.23, "y": 2342.45, "z": 70.67}, {"x": 5274.38, "y": 2342.19, "z": 70.68}, {"x": 5274.61, "y": 2341.9, "z": 70.69}, {"x": 5274.82, "y": 2341.43, "z": 70.68}, {"x": 5274.96, "y": 2341.19, "z": 70.68}, {"x": 5275.04, "y": 2340.73, "z": 70.69}, {"x": 5275.01, "y": 2340.32, "z": 70.71}], "right_lane_mark_type": "NONE", "successors": [38109482], "predecessors": [38117100], "right_neighbor_id": null, "left_neighbor_id": null}, "38109482": {"id": 38109482, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5279.45, "y": 2337.18, "z": 70.9}, {"x": 5279.21, "y": 2336.78, "z": 70.9}, {"x": 5278.95, "y": 2336.41, "z": 70.9}, {"x": 5274.25, "y": 2328.25, "z": 71.24}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5275.01, "y": 2340.32, "z": 70.71}, {"x": 5272.96, "y": 2337.26, "z": 70.88}, {"x": 5269.43, "y": 2331.55, "z": 71.14}], "right_lane_mark_type": "NONE", "successors": [38115599], "predecessors": [38109440, 38111601, 38111937], "right_neighbor_id": null, "left_neighbor_id": null}, "38109519": {"id": 38109519, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5286.78, "y": 2342.58, "z": 71.04}, {"x": 5272.94, "y": 2353.69, "z": 70.51}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5288.86, "y": 2345.49, "z": 70.99}, {"x": 5275.94, "y": 2358.54, "z": 70.49}], "right_lane_mark_type": "NONE", "successors": [38109382], "predecessors": [38109234], "right_neighbor_id": null, "left_neighbor_id": 38109167}, "38109698": {"id": 38109698, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5327.53, "y": 2329.17, "z": 72.01}, {"x": 5326.4, "y": 2327.28, "z": 72.0}, {"x": 5325.75, "y": 2326.62, "z": 71.96}, {"x": 5325.08, "y": 2326.02, "z": 71.94}, {"x": 5324.39, "y": 2325.46, "z": 71.96}, {"x": 5323.68, "y": 2324.97, "z": 71.97}, {"x": 5322.94, "y": 2324.52, "z": 71.99}, {"x": 5322.18, "y": 2324.14, "z": 72.0}, {"x": 5321.4, "y": 2323.82, "z": 72.0}, {"x": 5320.61, "y": 2323.56, "z": 72.0}, {"x": 5319.8, "y": 2323.37, "z": 72.0}, {"x": 5318.98, "y": 2323.24, "z": 72.0}, {"x": 5318.14, "y": 2323.19, "z": 71.99}, {"x": 5317.29, "y": 2323.21, "z": 71.98}, {"x": 5316.43, "y": 2323.31, "z": 71.97}, {"x": 5315.57, "y": 2323.48, "z": 71.96}, {"x": 5314.69, "y": 2323.73, "z": 71.94}, {"x": 5313.81, "y": 2324.07, "z": 71.93}, {"x": 5312.93, "y": 2324.49, "z": 71.91}, {"x": 5312.09, "y": 2325.02, "z": 71.89}, {"x": 5311.19, "y": 2325.66, "z": 71.86}, {"x": 5310.26, "y": 2326.29, "z": 71.83}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5320.76, "y": 2333.27, "z": 71.87}, {"x": 5319.93, "y": 2331.63, "z": 71.85}, {"x": 5319.51, "y": 2330.85, "z": 71.84}, {"x": 5319.0, "y": 2330.16, "z": 71.83}, {"x": 5318.4, "y": 2329.56, "z": 71.84}, {"x": 5317.73, "y": 2329.07, "z": 71.86}, {"x": 5316.85, "y": 2328.68, "z": 71.88}, {"x": 5316.01, "y": 2328.54, "z": 71.87}, {"x": 5315.24, "y": 2328.53, "z": 71.87}, {"x": 5314.22, "y": 2328.58, "z": 71.85}, {"x": 5313.34, "y": 2328.68, "z": 71.83}, {"x": 5312.53, "y": 2328.88, "z": 71.81}, {"x": 5312.04, "y": 2329.04, "z": 71.8}], "right_lane_mark_type": "NONE", "successors": [38111133], "predecessors": [38109176], "right_neighbor_id": null, "left_neighbor_id": null}, "38109824": {"id": 38109824, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5244.23, "y": 2400.0, "z": 69.18}, {"x": 5247.77, "y": 2404.68, "z": 69.27}, {"x": 5257.14, "y": 2419.46, "z": 69.48}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5251.14, "y": 2400.0, "z": 69.24}, {"x": 5262.48, "y": 2415.83, "z": 69.51}], "right_lane_mark_type": "NONE", "successors": [38114712, 38114672], "predecessors": [38114332], "right_neighbor_id": null, "left_neighbor_id": null}, "38110982": {"id": 38110982, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5205.75, "y": 2399.69, "z": 67.93}, {"x": 5185.87, "y": 2413.3, "z": 67.05}, {"x": 5180.46, "y": 2416.73, "z": 66.82}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5209.27, "y": 2404.3, "z": 67.86}, {"x": 5193.6, "y": 2415.04, "z": 67.19}, {"x": 5184.25, "y": 2421.43, "z": 66.78}], "right_lane_mark_type": "NONE", "successors": [38111662], "predecessors": [38114432], "right_neighbor_id": null, "left_neighbor_id": 38133156}, "38110983": {"id": 38110983, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5135.52, "y": 2449.51, "z": 64.99}, {"x": 5130.0, "y": 2453.5, "z": 64.81}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5137.92, "y": 2452.76, "z": 64.93}, {"x": 5130.0, "y": 2458.55, "z": 64.7}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111258], "predecessors": [38111696], "right_neighbor_id": 38111058, "left_neighbor_id": 38110984}, "38110984": {"id": 38110984, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5133.72, "y": 2446.99, "z": 64.98}, {"x": 5130.0, "y": 2449.64, "z": 64.84}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5135.52, "y": 2449.51, "z": 64.99}, {"x": 5130.0, "y": 2453.5, "z": 64.81}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111598], "predecessors": [38111213], "right_neighbor_id": 38110983, "left_neighbor_id": null}, "38110986": {"id": 38110986, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5140.87, "y": 2441.56, "z": 65.24}, {"x": 5147.02, "y": 2437.33, "z": 65.49}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5139.07, "y": 2438.93, "z": 65.22}, {"x": 5145.22, "y": 2434.75, "z": 65.49}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111243], "predecessors": [38111695], "right_neighbor_id": 38111020, "left_neighbor_id": null}, "38111000": {"id": 38111000, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5113.51, "y": 2473.63, "z": 64.13}, {"x": 5110.03, "y": 2476.77, "z": 63.99}, {"x": 5101.3, "y": 2485.1, "z": 63.73}, {"x": 5095.12, "y": 2491.1, "z": 63.43}, {"x": 5090.57, "y": 2495.35, "z": 63.27}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5116.24, "y": 2475.72, "z": 64.08}, {"x": 5112.65, "y": 2479.28, "z": 63.95}, {"x": 5105.8, "y": 2485.73, "z": 63.68}, {"x": 5093.09, "y": 2497.8, "z": 63.15}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111526], "predecessors": [38111648], "right_neighbor_id": null, "left_neighbor_id": null}, "38111004": {"id": 38111004, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5130.0, "y": 2458.55, "z": 64.7}, {"x": 5124.75, "y": 2462.33, "z": 64.55}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5130.0, "y": 2460.45, "z": 64.65}, {"x": 5125.66, "y": 2463.59, "z": 64.52}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111825, 38111786], "predecessors": [38111058], "right_neighbor_id": null, "left_neighbor_id": 38111258}, "38111020": {"id": 38111020, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5139.07, "y": 2438.93, "z": 65.22}, {"x": 5145.22, "y": 2434.75, "z": 65.49}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5137.14, "y": 2436.07, "z": 65.21}, {"x": 5143.22, "y": 2431.87, "z": 65.46}], "right_lane_mark_type": "NONE", "successors": [38111512], "predecessors": [38111212], "right_neighbor_id": null, "left_neighbor_id": 38110986}, "38111058": {"id": 38111058, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5137.92, "y": 2452.76, "z": 64.93}, {"x": 5130.0, "y": 2458.55, "z": 64.7}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5138.82, "y": 2453.97, "z": 64.91}, {"x": 5130.0, "y": 2460.45, "z": 64.65}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111004], "predecessors": [38111126], "right_neighbor_id": null, "left_neighbor_id": 38110983}, "38111090": {"id": 38111090, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5130.0, "y": 2449.04, "z": 64.84}, {"x": 5133.55, "y": 2446.6, "z": 64.97}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5130.0, "y": 2445.16, "z": 64.86}, {"x": 5131.72, "y": 2443.93, "z": 64.94}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111695], "predecessors": [38111649], "right_neighbor_id": 38111405, "left_neighbor_id": null}, "38111103": {"id": 38111103, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5304.84, "y": 2330.0, "z": 71.66}, {"x": 5310.26, "y": 2326.29, "z": 71.83}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5303.12, "y": 2327.48, "z": 71.65}, {"x": 5308.45, "y": 2323.82, "z": 71.8}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38109317, 38109290], "predecessors": [38109400], "right_neighbor_id": 38111861, "left_neighbor_id": 38111133}, "38111117": {"id": 38111117, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5111.95, "y": 2461.87, "z": 64.1}, {"x": 5101.54, "y": 2468.99, "z": 63.67}, {"x": 5096.29, "y": 2472.64, "z": 63.44}, {"x": 5092.62, "y": 2475.23, "z": 63.29}, {"x": 5090.22, "y": 2476.84, "z": 63.2}, {"x": 5088.24, "y": 2478.13, "z": 63.11}, {"x": 5087.42, "y": 2478.65, "z": 63.08}, {"x": 5086.51, "y": 2479.21, "z": 63.05}, {"x": 5085.43, "y": 2479.83, "z": 63.01}, {"x": 5084.47, "y": 2480.36, "z": 62.98}, {"x": 5083.22, "y": 2480.91, "z": 62.92}, {"x": 5081.58, "y": 2481.66, "z": 62.86}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5113.84, "y": 2464.62, "z": 64.19}, {"x": 5108.05, "y": 2468.59, "z": 63.95}, {"x": 5102.35, "y": 2472.56, "z": 63.72}, {"x": 5096.35, "y": 2476.77, "z": 63.47}, {"x": 5094.43, "y": 2478.06, "z": 63.39}, {"x": 5091.61, "y": 2479.89, "z": 63.27}, {"x": 5089.72, "y": 2481.04, "z": 63.2}, {"x": 5088.43, "y": 2481.86, "z": 63.14}, {"x": 5087.28, "y": 2482.56, "z": 63.09}, {"x": 5086.34, "y": 2483.09, "z": 63.05}, {"x": 5085.14, "y": 2483.71, "z": 63.01}, {"x": 5084.23, "y": 2484.17, "z": 62.98}, {"x": 5083.14, "y": 2484.66, "z": 62.94}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111164], "predecessors": [38111770], "right_neighbor_id": 38111615, "left_neighbor_id": null}, "38111126": {"id": 38111126, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5145.11, "y": 2447.42, "z": 65.17}, {"x": 5137.92, "y": 2452.76, "z": 64.93}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5145.97, "y": 2448.56, "z": 65.22}, {"x": 5138.82, "y": 2453.97, "z": 64.91}], "right_lane_mark_type": "NONE", "successors": [38111058], "predecessors": [38111629], "right_neighbor_id": null, "left_neighbor_id": 38111696}, "38111133": {"id": 38111133, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5310.26, "y": 2326.29, "z": 71.83}, {"x": 5304.84, "y": 2330.0, "z": 71.66}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5312.04, "y": 2329.04, "z": 71.8}, {"x": 5306.47, "y": 2332.86, "z": 71.62}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38109234], "predecessors": [38109698, 38109397], "right_neighbor_id": 38111905, "left_neighbor_id": 38111103}, "38111163": {"id": 38111163, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5130.0, "y": 2456.9, "z": 64.75}, {"x": 5124.76, "y": 2462.32, "z": 64.55}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5130.0, "y": 2462.47, "z": 64.59}, {"x": 5126.93, "y": 2465.41, "z": 64.47}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111228], "predecessors": [38114770], "right_neighbor_id": null, "left_neighbor_id": null}, "38111173": {"id": 38111173, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5148.71, "y": 2439.81, "z": 65.47}, {"x": 5142.73, "y": 2444.2, "z": 65.25}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5150.88, "y": 2443.13, "z": 65.39}, {"x": 5145.11, "y": 2447.42, "z": 65.17}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111696], "predecessors": [38111884, 38111446], "right_neighbor_id": 38111629, "left_neighbor_id": 38111540}, "38111175": {"id": 38111175, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.03, "y": 2426.94, "z": 66.29}, {"x": 5147.27, "y": 2437.68, "z": 65.49}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5169.46, "y": 2431.57, "z": 66.22}, {"x": 5148.71, "y": 2439.81, "z": 65.47}], "right_lane_mark_type": "NONE", "successors": [38111540], "predecessors": [38111662], "right_neighbor_id": null, "left_neighbor_id": null}, "38111212": {"id": 38111212, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5131.72, "y": 2443.93, "z": 64.94}, {"x": 5139.07, "y": 2438.93, "z": 65.22}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5131.33, "y": 2440.05, "z": 64.91}, {"x": 5137.14, "y": 2436.07, "z": 65.21}], "right_lane_mark_type": "NONE", "successors": [38111020], "predecessors": [38111405], "right_neighbor_id": null, "left_neighbor_id": 38111695}, "38111213": {"id": 38111213, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5141.1, "y": 2441.92, "z": 65.25}, {"x": 5133.72, "y": 2446.99, "z": 64.98}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5142.73, "y": 2444.2, "z": 65.25}, {"x": 5135.52, "y": 2449.51, "z": 64.99}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38110984], "predecessors": [38111540], "right_neighbor_id": 38111696, "left_neighbor_id": null}, "38111228": {"id": 38111228, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5124.76, "y": 2462.32, "z": 64.55}, {"x": 5117.46, "y": 2469.61, "z": 64.25}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5126.93, "y": 2465.41, "z": 64.47}, {"x": 5119.56, "y": 2472.48, "z": 64.2}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111648], "predecessors": [38111163], "right_neighbor_id": null, "left_neighbor_id": null}, "38111243": {"id": 38111243, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5147.02, "y": 2437.33, "z": 65.49}, {"x": 5164.69, "y": 2425.75, "z": 66.29}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5145.22, "y": 2434.75, "z": 65.49}, {"x": 5160.51, "y": 2419.95, "z": 66.15}], "right_lane_mark_type": "NONE", "successors": [38133154, 38133155], "predecessors": [38110986], "right_neighbor_id": null, "left_neighbor_id": null}, "38111258": {"id": 38111258, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5130.0, "y": 2453.5, "z": 64.81}, {"x": 5122.33, "y": 2458.78, "z": 64.51}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5130.0, "y": 2458.55, "z": 64.7}, {"x": 5124.75, "y": 2462.33, "z": 64.55}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111737], "predecessors": [38110983], "right_neighbor_id": 38111004, "left_neighbor_id": 38111598}, "38111259": {"id": 38111259, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5079.73, "y": 2477.78, "z": 62.79}, {"x": 5081.13, "y": 2477.18, "z": 62.83}, {"x": 5083.03, "y": 2476.35, "z": 62.91}, {"x": 5085.05, "y": 2475.33, "z": 62.98}, {"x": 5087.4, "y": 2474.04, "z": 63.07}, {"x": 5092.55, "y": 2470.61, "z": 63.28}, {"x": 5100.04, "y": 2465.55, "z": 63.61}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5078.14, "y": 2474.28, "z": 62.74}, {"x": 5083.13, "y": 2472.18, "z": 62.87}, {"x": 5084.01, "y": 2471.7, "z": 62.9}, {"x": 5086.97, "y": 2469.92, "z": 63.0}, {"x": 5091.69, "y": 2466.83, "z": 63.17}, {"x": 5095.67, "y": 2464.08, "z": 63.37}, {"x": 5097.86, "y": 2462.61, "z": 63.48}], "right_lane_mark_type": "NONE", "successors": [38111774], "predecessors": [38111397], "right_neighbor_id": null, "left_neighbor_id": 38111425}, "38111278": {"id": 38111278, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5166.03, "y": 2426.94, "z": 66.29}, {"x": 5150.88, "y": 2443.13, "z": 65.39}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5169.46, "y": 2431.57, "z": 66.22}, {"x": 5166.8, "y": 2433.46, "z": 66.08}, {"x": 5166.46, "y": 2433.75, "z": 66.08}, {"x": 5151.76, "y": 2444.37, "z": 65.42}], "right_lane_mark_type": "NONE", "successors": [38111629], "predecessors": [38111662], "right_neighbor_id": null, "left_neighbor_id": null}, "38111310": {"id": 38111310, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.37, "y": 2440.79, "z": 65.95}, {"x": 5152.69, "y": 2421.54, "z": 65.79}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5160.14, "y": 2445.81, "z": 65.74}, {"x": 5146.08, "y": 2426.08, "z": 65.61}], "right_lane_mark_type": "NONE", "successors": [38111342], "predecessors": [38119951], "right_neighbor_id": null, "left_neighbor_id": null}, "38111330": {"id": 38111330, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5116.4, "y": 2468.26, "z": 64.25}, {"x": 5111.95, "y": 2471.45, "z": 64.09}, {"x": 5109.72, "y": 2473.07, "z": 64.02}, {"x": 5107.1, "y": 2474.87, "z": 63.9}, {"x": 5103.61, "y": 2477.16, "z": 63.76}, {"x": 5099.76, "y": 2479.78, "z": 63.6}, {"x": 5095.35, "y": 2482.63, "z": 63.41}, {"x": 5091.99, "y": 2484.45, "z": 63.28}, {"x": 5088.37, "y": 2486.26, "z": 63.15}, {"x": 5084.78, "y": 2487.92, "z": 63.01}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5117.29, "y": 2469.51, "z": 64.25}, {"x": 5115.69, "y": 2470.61, "z": 64.2}, {"x": 5113.0, "y": 2472.51, "z": 64.17}, {"x": 5112.11, "y": 2473.06, "z": 64.17}, {"x": 5107.23, "y": 2476.31, "z": 63.98}, {"x": 5104.65, "y": 2478.19, "z": 63.96}, {"x": 5100.95, "y": 2480.67, "z": 63.77}, {"x": 5098.57, "y": 2482.18, "z": 63.67}, {"x": 5095.81, "y": 2484.07, "z": 63.53}, {"x": 5090.71, "y": 2486.92, "z": 63.34}, {"x": 5085.49, "y": 2489.18, "z": 63.08}], "right_lane_mark_type": "NONE", "successors": [38111783, 38111430], "predecessors": [38111786], "right_neighbor_id": null, "left_neighbor_id": 38111615}, "38111342": {"id": 38111342, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5152.69, "y": 2421.54, "z": 65.79}, {"x": 5151.57, "y": 2420.03, "z": 65.75}, {"x": 5137.17, "y": 2396.17, "z": 65.11}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5146.08, "y": 2426.08, "z": 65.61}, {"x": 5145.96, "y": 2425.82, "z": 65.61}, {"x": 5142.05, "y": 2419.45, "z": 65.43}, {"x": 5130.4, "y": 2400.29, "z": 64.88}], "right_lane_mark_type": "NONE", "successors": [38118957, 38119599], "predecessors": [38111512, 38111880, 38111310], "right_neighbor_id": null, "left_neighbor_id": null}, "38111376": {"id": 38111376, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5201.69, "y": 2490.0, "z": 66.87}, {"x": 5192.71, "y": 2477.54, "z": 66.59}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5191.84, "y": 2490.0, "z": 66.62}, {"x": 5186.24, "y": 2482.2, "z": 66.46}], "right_lane_mark_type": "NONE", "successors": [38119952, 38119951], "predecessors": [38117242], "right_neighbor_id": null, "left_neighbor_id": null}, "38111405": {"id": 38111405, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5130.0, "y": 2445.16, "z": 64.86}, {"x": 5131.72, "y": 2443.93, "z": 64.94}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5130.0, "y": 2440.97, "z": 64.85}, {"x": 5131.33, "y": 2440.05, "z": 64.91}], "right_lane_mark_type": "NONE", "successors": [38111212], "predecessors": [38111604], "right_neighbor_id": null, "left_neighbor_id": 38111090}, "38111425": {"id": 38111425, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5081.26, "y": 2481.11, "z": 62.84}, {"x": 5082.05, "y": 2480.79, "z": 62.88}, {"x": 5082.85, "y": 2480.47, "z": 62.91}, {"x": 5084.73, "y": 2479.52, "z": 62.99}, {"x": 5086.17, "y": 2478.74, "z": 63.03}, {"x": 5087.7, "y": 2477.81, "z": 63.09}, {"x": 5089.18, "y": 2476.86, "z": 63.15}, {"x": 5092.88, "y": 2474.36, "z": 63.3}, {"x": 5101.82, "y": 2468.17, "z": 63.68}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5079.73, "y": 2477.78, "z": 62.79}, {"x": 5081.13, "y": 2477.18, "z": 62.83}, {"x": 5083.03, "y": 2476.35, "z": 62.91}, {"x": 5085.05, "y": 2475.33, "z": 62.98}, {"x": 5087.4, "y": 2474.04, "z": 63.07}, {"x": 5092.55, "y": 2470.61, "z": 63.28}, {"x": 5100.04, "y": 2465.55, "z": 63.61}], "right_lane_mark_type": "NONE", "successors": [38111781], "predecessors": [38111197, 38111398], "right_neighbor_id": 38111259, "left_neighbor_id": null}, "38111446": {"id": 38111446, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.03, "y": 2426.94, "z": 66.29}, {"x": 5148.71, "y": 2439.81, "z": 65.47}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5169.46, "y": 2431.57, "z": 66.22}, {"x": 5150.88, "y": 2443.13, "z": 65.39}], "right_lane_mark_type": "NONE", "successors": [38111173], "predecessors": [38111662], "right_neighbor_id": null, "left_neighbor_id": null}, "38111512": {"id": 38111512, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5145.22, "y": 2434.75, "z": 65.49}, {"x": 5145.96, "y": 2434.28, "z": 65.52}, {"x": 5146.76, "y": 2433.8, "z": 65.56}, {"x": 5147.61, "y": 2433.29, "z": 65.59}, {"x": 5148.43, "y": 2432.77, "z": 65.63}, {"x": 5149.21, "y": 2432.22, "z": 65.66}, {"x": 5149.95, "y": 2431.61, "z": 65.7}, {"x": 5150.65, "y": 2430.97, "z": 65.74}, {"x": 5151.3, "y": 2430.3, "z": 65.76}, {"x": 5151.9, "y": 2429.6, "z": 65.79}, {"x": 5152.43, "y": 2428.88, "z": 65.81}, {"x": 5152.88, "y": 2428.14, "z": 65.83}, {"x": 5153.25, "y": 2427.37, "z": 65.84}, {"x": 5153.52, "y": 2426.58, "z": 65.85}, {"x": 5153.7, "y": 2425.78, "z": 65.85}, {"x": 5153.76, "y": 2424.95, "z": 65.86}, {"x": 5153.69, "y": 2424.12, "z": 65.84}, {"x": 5153.5, "y": 2423.27, "z": 65.81}, {"x": 5153.17, "y": 2422.41, "z": 65.79}, {"x": 5152.69, "y": 2421.54, "z": 65.79}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5143.22, "y": 2431.87, "z": 65.46}, {"x": 5144.12, "y": 2431.28, "z": 65.51}, {"x": 5144.97, "y": 2430.71, "z": 65.53}, {"x": 5145.59, "y": 2430.3, "z": 65.57}, {"x": 5145.67, "y": 2430.2, "z": 65.57}, {"x": 5146.17, "y": 2429.6, "z": 65.57}, {"x": 5146.53, "y": 2428.81, "z": 65.58}, {"x": 5146.59, "y": 2427.88, "z": 65.59}, {"x": 5146.52, "y": 2427.28, "z": 65.6}, {"x": 5146.27, "y": 2426.51, "z": 65.6}, {"x": 5146.08, "y": 2426.08, "z": 65.61}], "right_lane_mark_type": "NONE", "successors": [38111342], "predecessors": [38111020], "right_neighbor_id": null, "left_neighbor_id": null}, "38111540": {"id": 38111540, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5147.27, "y": 2437.68, "z": 65.49}, {"x": 5141.1, "y": 2441.92, "z": 65.25}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5148.71, "y": 2439.81, "z": 65.47}, {"x": 5142.73, "y": 2444.2, "z": 65.25}], "right_lane_mark_type": "NONE", "successors": [38111213], "predecessors": [38111175], "right_neighbor_id": 38111173, "left_neighbor_id": null}, "38111569": {"id": 38111569, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5066.52, "y": 2466.66, "z": 62.14}, {"x": 5068.47, "y": 2467.59, "z": 62.23}, {"x": 5070.87, "y": 2468.57, "z": 62.34}, {"x": 5072.29, "y": 2469.01, "z": 62.4}, {"x": 5073.81, "y": 2469.29, "z": 62.46}, {"x": 5076.67, "y": 2469.75, "z": 62.54}, {"x": 5079.26, "y": 2470.07, "z": 62.65}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5069.85, "y": 2462.07, "z": 62.04}, {"x": 5071.48, "y": 2462.83, "z": 62.13}, {"x": 5073.5, "y": 2463.69, "z": 62.24}, {"x": 5075.66, "y": 2464.42, "z": 62.35}, {"x": 5077.95, "y": 2464.95, "z": 62.47}, {"x": 5080.57, "y": 2465.35, "z": 62.6}, {"x": 5085.44, "y": 2465.44, "z": 62.84}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111751], "predecessors": [38111571], "right_neighbor_id": null, "left_neighbor_id": null}, "38111598": {"id": 38111598, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5130.0, "y": 2449.64, "z": 64.84}, {"x": 5120.49, "y": 2456.1, "z": 64.43}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5130.0, "y": 2453.5, "z": 64.81}, {"x": 5122.33, "y": 2458.78, "z": 64.51}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111770], "predecessors": [38110984], "right_neighbor_id": 38111258, "left_neighbor_id": null}, "38111601": {"id": 38111601, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5286.78, "y": 2342.58, "z": 71.04}, {"x": 5285.92, "y": 2342.62, "z": 71.02}, {"x": 5285.09, "y": 2342.48, "z": 71.01}, {"x": 5284.29, "y": 2342.18, "z": 70.99}, {"x": 5283.52, "y": 2341.75, "z": 70.97}, {"x": 5282.79, "y": 2341.21, "z": 70.96}, {"x": 5282.1, "y": 2340.58, "z": 70.94}, {"x": 5281.46, "y": 2339.91, "z": 70.93}, {"x": 5280.87, "y": 2339.2, "z": 70.91}, {"x": 5280.33, "y": 2338.49, "z": 70.88}, {"x": 5279.86, "y": 2337.81, "z": 70.87}, {"x": 5279.45, "y": 2337.18, "z": 70.9}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5288.86, "y": 2345.49, "z": 70.99}, {"x": 5287.83, "y": 2346.05, "z": 70.96}, {"x": 5287.28, "y": 2346.33, "z": 70.94}, {"x": 5286.71, "y": 2346.55, "z": 70.93}, {"x": 5285.93, "y": 2346.6, "z": 70.91}, {"x": 5285.2, "y": 2346.62, "z": 70.9}, {"x": 5284.48, "y": 2346.61, "z": 70.89}, {"x": 5283.65, "y": 2346.51, "z": 70.88}, {"x": 5282.79, "y": 2346.29, "z": 70.87}, {"x": 5281.93, "y": 2345.99, "z": 70.87}, {"x": 5281.09, "y": 2345.61, "z": 70.85}, {"x": 5280.26, "y": 2345.16, "z": 70.84}, {"x": 5279.47, "y": 2344.65, "z": 70.83}, {"x": 5278.7, "y": 2344.09, "z": 70.82}, {"x": 5277.97, "y": 2343.5, "z": 70.79}, {"x": 5277.28, "y": 2342.88, "z": 70.77}, {"x": 5276.63, "y": 2342.24, "z": 70.74}, {"x": 5276.03, "y": 2341.59, "z": 70.71}, {"x": 5275.49, "y": 2340.95, "z": 70.69}, {"x": 5275.01, "y": 2340.32, "z": 70.71}], "right_lane_mark_type": "NONE", "successors": [38109482], "predecessors": [38109234], "right_neighbor_id": null, "left_neighbor_id": null}, "38111604": {"id": 38111604, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5119.85, "y": 2452.02, "z": 64.44}, {"x": 5130.0, "y": 2445.16, "z": 64.86}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5117.9, "y": 2449.2, "z": 64.37}, {"x": 5120.85, "y": 2447.19, "z": 64.47}, {"x": 5121.09, "y": 2447.05, "z": 64.47}, {"x": 5121.3, "y": 2446.87, "z": 64.48}, {"x": 5121.29, "y": 2446.81, "z": 64.49}, {"x": 5130.0, "y": 2440.97, "z": 64.85}], "right_lane_mark_type": "NONE", "successors": [38111405], "predecessors": [38111774], "right_neighbor_id": null, "left_neighbor_id": 38111649}, "38111615": {"id": 38111615, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5113.84, "y": 2464.62, "z": 64.19}, {"x": 5108.05, "y": 2468.59, "z": 63.95}, {"x": 5102.35, "y": 2472.56, "z": 63.72}, {"x": 5096.35, "y": 2476.77, "z": 63.47}, {"x": 5094.43, "y": 2478.06, "z": 63.39}, {"x": 5091.61, "y": 2479.89, "z": 63.27}, {"x": 5089.72, "y": 2481.04, "z": 63.2}, {"x": 5088.43, "y": 2481.86, "z": 63.14}, {"x": 5087.28, "y": 2482.56, "z": 63.09}, {"x": 5086.34, "y": 2483.09, "z": 63.05}, {"x": 5085.14, "y": 2483.71, "z": 63.01}, {"x": 5084.23, "y": 2484.17, "z": 62.98}, {"x": 5083.14, "y": 2484.66, "z": 62.94}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5116.4, "y": 2468.26, "z": 64.25}, {"x": 5111.95, "y": 2471.45, "z": 64.09}, {"x": 5109.72, "y": 2473.07, "z": 64.02}, {"x": 5107.1, "y": 2474.87, "z": 63.9}, {"x": 5103.61, "y": 2477.16, "z": 63.76}, {"x": 5099.76, "y": 2479.78, "z": 63.6}, {"x": 5095.35, "y": 2482.63, "z": 63.41}, {"x": 5091.99, "y": 2484.45, "z": 63.28}, {"x": 5088.37, "y": 2486.26, "z": 63.15}, {"x": 5084.78, "y": 2487.92, "z": 63.01}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111165], "predecessors": [38111737], "right_neighbor_id": 38111330, "left_neighbor_id": 38111117}, "38111629": {"id": 38111629, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5150.88, "y": 2443.13, "z": 65.39}, {"x": 5145.11, "y": 2447.42, "z": 65.17}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5151.76, "y": 2444.37, "z": 65.42}, {"x": 5147.97, "y": 2447.04, "z": 65.29}, {"x": 5145.97, "y": 2448.56, "z": 65.22}], "right_lane_mark_type": "NONE", "successors": [38111126], "predecessors": [38111873, 38111278], "right_neighbor_id": null, "left_neighbor_id": 38111173}, "38111648": {"id": 38111648, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5117.46, "y": 2469.61, "z": 64.25}, {"x": 5116.6, "y": 2470.47, "z": 64.22}, {"x": 5116.53, "y": 2470.59, "z": 64.22}, {"x": 5116.2, "y": 2470.87, "z": 64.2}, {"x": 5113.51, "y": 2473.63, "z": 64.13}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5119.56, "y": 2472.48, "z": 64.2}, {"x": 5116.24, "y": 2475.72, "z": 64.08}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111000], "predecessors": [38111825, 38111228], "right_neighbor_id": null, "left_neighbor_id": null}, "38111649": {"id": 38111649, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5121.66, "y": 2454.65, "z": 64.48}, {"x": 5130.0, "y": 2449.04, "z": 64.84}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5119.85, "y": 2452.02, "z": 64.44}, {"x": 5130.0, "y": 2445.16, "z": 64.86}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111090], "predecessors": [38111781], "right_neighbor_id": 38111604, "left_neighbor_id": null}, "38111662": {"id": 38111662, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5180.46, "y": 2416.73, "z": 66.82}, {"x": 5166.03, "y": 2426.94, "z": 66.29}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5184.25, "y": 2421.43, "z": 66.78}, {"x": 5170.51, "y": 2430.82, "z": 66.28}, {"x": 5169.46, "y": 2431.57, "z": 66.22}], "right_lane_mark_type": "NONE", "successors": [38111278, 38111446, 38111175, 38111880], "predecessors": [38110982], "right_neighbor_id": null, "left_neighbor_id": null}, "38111695": {"id": 38111695, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5133.55, "y": 2446.6, "z": 64.97}, {"x": 5140.87, "y": 2441.56, "z": 65.24}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5131.72, "y": 2443.93, "z": 64.94}, {"x": 5139.07, "y": 2438.93, "z": 65.22}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38110986], "predecessors": [38111090], "right_neighbor_id": 38111212, "left_neighbor_id": null}, "38111696": {"id": 38111696, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5142.73, "y": 2444.2, "z": 65.25}, {"x": 5135.52, "y": 2449.51, "z": 64.99}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5145.11, "y": 2447.42, "z": 65.17}, {"x": 5137.92, "y": 2452.76, "z": 64.93}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111898, 38110983], "predecessors": [38111173], "right_neighbor_id": 38111126, "left_neighbor_id": 38111213}, "38111737": {"id": 38111737, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5122.33, "y": 2458.78, "z": 64.51}, {"x": 5121.22, "y": 2459.55, "z": 64.47}, {"x": 5113.84, "y": 2464.62, "z": 64.19}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5124.75, "y": 2462.33, "z": 64.55}, {"x": 5123.67, "y": 2463.07, "z": 64.52}, {"x": 5117.45, "y": 2467.5, "z": 64.29}, {"x": 5116.4, "y": 2468.26, "z": 64.25}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111615], "predecessors": [38111258], "right_neighbor_id": 38111786, "left_neighbor_id": 38111770}, "38111751": {"id": 38111751, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5079.26, "y": 2470.07, "z": 62.65}, {"x": 5082.01, "y": 2470.27, "z": 62.77}, {"x": 5084.24, "y": 2470.3, "z": 62.87}, {"x": 5085.56, "y": 2470.28, "z": 62.94}, {"x": 5086.7, "y": 2470.21, "z": 62.99}, {"x": 5087.88, "y": 2470.09, "z": 63.05}, {"x": 5089.17, "y": 2469.89, "z": 63.1}, {"x": 5091.33, "y": 2469.43, "z": 63.21}, {"x": 5093.3, "y": 2468.83, "z": 63.3}, {"x": 5095.12, "y": 2468.12, "z": 63.37}, {"x": 5096.85, "y": 2467.34, "z": 63.46}, {"x": 5098.62, "y": 2466.33, "z": 63.54}, {"x": 5100.04, "y": 2465.55, "z": 63.61}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5085.44, "y": 2465.44, "z": 62.84}, {"x": 5088.67, "y": 2465.36, "z": 63.0}, {"x": 5090.39, "y": 2465.13, "z": 63.08}, {"x": 5091.98, "y": 2464.8, "z": 63.17}, {"x": 5093.72, "y": 2464.32, "z": 63.25}, {"x": 5094.59, "y": 2464.01, "z": 63.31}, {"x": 5095.69, "y": 2463.57, "z": 63.37}, {"x": 5097.2, "y": 2462.94, "z": 63.44}, {"x": 5097.86, "y": 2462.61, "z": 63.48}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111774], "predecessors": [38111569], "right_neighbor_id": null, "left_neighbor_id": null}, "38111770": {"id": 38111770, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5120.49, "y": 2456.1, "z": 64.43}, {"x": 5112.0, "y": 2461.84, "z": 64.1}, {"x": 5111.95, "y": 2461.87, "z": 64.1}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5122.33, "y": 2458.78, "z": 64.51}, {"x": 5121.22, "y": 2459.55, "z": 64.47}, {"x": 5113.84, "y": 2464.62, "z": 64.19}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111117], "predecessors": [38111598], "right_neighbor_id": 38111737, "left_neighbor_id": null}, "38111774": {"id": 38111774, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5100.04, "y": 2465.55, "z": 63.61}, {"x": 5119.85, "y": 2452.02, "z": 64.44}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5097.86, "y": 2462.61, "z": 63.48}, {"x": 5098.44, "y": 2462.26, "z": 63.51}, {"x": 5098.89, "y": 2461.99, "z": 63.53}, {"x": 5100.26, "y": 2461.14, "z": 63.59}, {"x": 5102.01, "y": 2460.06, "z": 63.63}, {"x": 5103.45, "y": 2459.11, "z": 63.73}, {"x": 5107.42, "y": 2456.44, "z": 63.94}, {"x": 5110.5, "y": 2454.3, "z": 64.08}, {"x": 5117.9, "y": 2449.2, "z": 64.37}], "right_lane_mark_type": "NONE", "successors": [38111604], "predecessors": [38111259, 38111751], "right_neighbor_id": null, "left_neighbor_id": 38111781}, "38111781": {"id": 38111781, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5101.82, "y": 2468.17, "z": 63.68}, {"x": 5121.66, "y": 2454.65, "z": 64.48}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5100.04, "y": 2465.55, "z": 63.61}, {"x": 5119.85, "y": 2452.02, "z": 64.44}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111649], "predecessors": [38111425], "right_neighbor_id": 38111774, "left_neighbor_id": null}, "38111786": {"id": 38111786, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5124.75, "y": 2462.33, "z": 64.55}, {"x": 5123.67, "y": 2463.07, "z": 64.52}, {"x": 5117.45, "y": 2467.5, "z": 64.29}, {"x": 5116.4, "y": 2468.26, "z": 64.25}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5125.66, "y": 2463.59, "z": 64.52}, {"x": 5125.25, "y": 2463.86, "z": 64.51}, {"x": 5119.45, "y": 2468.02, "z": 64.33}, {"x": 5117.29, "y": 2469.51, "z": 64.25}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111330], "predecessors": [38111004], "right_neighbor_id": null, "left_neighbor_id": 38111737}, "38111825": {"id": 38111825, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5124.75, "y": 2462.33, "z": 64.55}, {"x": 5117.46, "y": 2469.61, "z": 64.25}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5125.66, "y": 2463.59, "z": 64.52}, {"x": 5119.56, "y": 2472.48, "z": 64.2}], "right_lane_mark_type": "NONE", "successors": [38111648], "predecessors": [38111004], "right_neighbor_id": null, "left_neighbor_id": null}, "38111849": {"id": 38111849, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5308.45, "y": 2323.82, "z": 71.8}, {"x": 5328.5, "y": 2310.17, "z": 72.07}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5307.63, "y": 2322.37, "z": 71.78}, {"x": 5327.54, "y": 2308.94, "z": 72.09}], "right_lane_mark_type": "NONE", "successors": [38111857], "predecessors": [38111861], "right_neighbor_id": null, "left_neighbor_id": 38109290}, "38111855": {"id": 38111855, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5308.45, "y": 2323.82, "z": 71.8}, {"x": 5309.12, "y": 2323.27, "z": 71.82}, {"x": 5309.82, "y": 2322.77, "z": 71.84}, {"x": 5310.56, "y": 2322.26, "z": 71.86}, {"x": 5311.35, "y": 2321.74, "z": 71.88}, {"x": 5312.17, "y": 2321.21, "z": 71.9}, {"x": 5313.01, "y": 2320.67, "z": 71.92}, {"x": 5313.87, "y": 2320.11, "z": 71.95}, {"x": 5314.74, "y": 2319.53, "z": 71.97}, {"x": 5315.6, "y": 2318.94, "z": 71.98}, {"x": 5316.44, "y": 2318.34, "z": 72.0}, {"x": 5317.27, "y": 2317.71, "z": 72.01}, {"x": 5318.06, "y": 2317.07, "z": 72.02}, {"x": 5318.82, "y": 2316.41, "z": 72.03}, {"x": 5319.52, "y": 2315.73, "z": 72.04}, {"x": 5320.16, "y": 2315.02, "z": 72.05}, {"x": 5320.74, "y": 2314.29, "z": 72.07}, {"x": 5321.24, "y": 2313.54, "z": 72.08}, {"x": 5321.65, "y": 2312.77, "z": 72.1}, {"x": 5321.97, "y": 2311.97, "z": 72.11}, {"x": 5322.19, "y": 2311.14, "z": 72.13}, {"x": 5322.29, "y": 2310.29, "z": 72.14}, {"x": 5322.27, "y": 2309.41, "z": 72.15}, {"x": 5322.11, "y": 2308.5, "z": 72.16}, {"x": 5321.82, "y": 2307.56, "z": 72.2}, {"x": 5321.42, "y": 2306.55, "z": 72.28}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5307.63, "y": 2322.37, "z": 71.78}, {"x": 5307.83, "y": 2322.21, "z": 71.79}, {"x": 5308.23, "y": 2321.9, "z": 71.8}, {"x": 5308.8, "y": 2321.46, "z": 71.82}, {"x": 5309.5, "y": 2320.9, "z": 71.84}, {"x": 5310.29, "y": 2320.24, "z": 71.86}, {"x": 5311.16, "y": 2319.5, "z": 71.89}, {"x": 5312.05, "y": 2318.69, "z": 71.91}, {"x": 5312.95, "y": 2317.84, "z": 71.94}, {"x": 5313.81, "y": 2316.95, "z": 71.97}, {"x": 5314.6, "y": 2316.05, "z": 71.99}, {"x": 5315.29, "y": 2315.15, "z": 72.01}, {"x": 5315.84, "y": 2314.28, "z": 72.03}, {"x": 5316.23, "y": 2313.43, "z": 72.06}, {"x": 5316.42, "y": 2312.64, "z": 72.09}, {"x": 5316.37, "y": 2311.92, "z": 72.13}, {"x": 5315.57, "y": 2310.62, "z": 72.21}], "right_lane_mark_type": "NONE", "successors": [38109302], "predecessors": [38111861], "right_neighbor_id": null, "left_neighbor_id": null}, "38111858": {"id": 38111858, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5272.94, "y": 2353.69, "z": 70.51}, {"x": 5285.11, "y": 2340.16, "z": 71.03}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5268.73, "y": 2346.16, "z": 70.46}, {"x": 5284.05, "y": 2338.62, "z": 71.01}], "right_lane_mark_type": "NONE", "successors": [38111866], "predecessors": [38117100], "right_neighbor_id": null, "left_neighbor_id": null}, "38111861": {"id": 38111861, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5303.12, "y": 2327.48, "z": 71.65}, {"x": 5308.45, "y": 2323.82, "z": 71.8}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5302.21, "y": 2326.11, "z": 71.63}, {"x": 5307.63, "y": 2322.37, "z": 71.78}], "right_lane_mark_type": "NONE", "successors": [38111849, 38111855], "predecessors": [38111866], "right_neighbor_id": null, "left_neighbor_id": 38111103}, "38111866": {"id": 38111866, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5285.11, "y": 2340.16, "z": 71.03}, {"x": 5303.12, "y": 2327.48, "z": 71.65}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5284.05, "y": 2338.62, "z": 71.01}, {"x": 5288.66, "y": 2335.47, "z": 71.16}, {"x": 5302.21, "y": 2326.11, "z": 71.63}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111861], "predecessors": [38111858], "right_neighbor_id": null, "left_neighbor_id": 38109400}, "38111873": {"id": 38111873, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5166.37, "y": 2440.79, "z": 65.95}, {"x": 5165.77, "y": 2440.09, "z": 65.99}, {"x": 5165.03, "y": 2439.61, "z": 65.9}, {"x": 5164.25, "y": 2439.24, "z": 65.89}, {"x": 5163.45, "y": 2438.98, "z": 65.87}, {"x": 5162.64, "y": 2438.81, "z": 65.86}, {"x": 5161.8, "y": 2438.74, "z": 65.84}, {"x": 5160.95, "y": 2438.75, "z": 65.82}, {"x": 5160.09, "y": 2438.84, "z": 65.79}, {"x": 5159.22, "y": 2439.0, "z": 65.76}, {"x": 5158.35, "y": 2439.23, "z": 65.72}, {"x": 5157.48, "y": 2439.51, "z": 65.68}, {"x": 5156.61, "y": 2439.85, "z": 65.65}, {"x": 5155.74, "y": 2440.24, "z": 65.6}, {"x": 5154.89, "y": 2440.66, "z": 65.56}, {"x": 5154.05, "y": 2441.12, "z": 65.52}, {"x": 5153.22, "y": 2441.6, "z": 65.48}, {"x": 5152.42, "y": 2442.1, "z": 65.45}, {"x": 5150.88, "y": 2443.13, "z": 65.39}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5160.14, "y": 2445.81, "z": 65.74}, {"x": 5159.4, "y": 2444.83, "z": 65.71}, {"x": 5159.0, "y": 2444.27, "z": 65.67}, {"x": 5158.73, "y": 2443.96, "z": 65.67}, {"x": 5158.13, "y": 2443.33, "z": 65.63}, {"x": 5157.03, "y": 2442.76, "z": 65.56}, {"x": 5156.59, "y": 2442.67, "z": 65.55}, {"x": 5155.62, "y": 2442.36, "z": 65.53}, {"x": 5154.62, "y": 2442.67, "z": 65.54}, {"x": 5154.25, "y": 2442.73, "z": 65.52}, {"x": 5152.85, "y": 2443.67, "z": 65.47}, {"x": 5151.76, "y": 2444.37, "z": 65.42}], "right_lane_mark_type": "NONE", "successors": [38111629], "predecessors": [38119951], "right_neighbor_id": null, "left_neighbor_id": null}, "38111879": {"id": 38111879, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.37, "y": 2440.79, "z": 65.95}, {"x": 5165.82, "y": 2440.03, "z": 66.0}, {"x": 5165.32, "y": 2439.21, "z": 65.95}, {"x": 5164.85, "y": 2438.56, "z": 65.92}, {"x": 5164.31, "y": 2437.77, "z": 65.9}, {"x": 5163.79, "y": 2436.95, "z": 65.88}, {"x": 5163.3, "y": 2436.1, "z": 65.88}, {"x": 5162.86, "y": 2435.24, "z": 65.91}, {"x": 5162.47, "y": 2434.36, "z": 65.95}, {"x": 5162.14, "y": 2433.48, "z": 65.98}, {"x": 5161.89, "y": 2432.6, "z": 66.01}, {"x": 5161.72, "y": 2431.73, "z": 66.03}, {"x": 5161.64, "y": 2430.87, "z": 66.06}, {"x": 5161.68, "y": 2430.03, "z": 66.09}, {"x": 5161.83, "y": 2429.21, "z": 66.12}, {"x": 5162.1, "y": 2428.43, "z": 66.15}, {"x": 5162.51, "y": 2427.68, "z": 66.18}, {"x": 5163.07, "y": 2426.98, "z": 66.22}, {"x": 5163.79, "y": 2426.34, "z": 66.25}, {"x": 5164.69, "y": 2425.75, "z": 66.29}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5160.14, "y": 2445.81, "z": 65.74}, {"x": 5158.73, "y": 2443.96, "z": 65.67}, {"x": 5158.61, "y": 2443.83, "z": 65.66}, {"x": 5158.04, "y": 2442.97, "z": 65.6}, {"x": 5157.55, "y": 2442.18, "z": 65.58}, {"x": 5157.08, "y": 2441.36, "z": 65.59}, {"x": 5156.63, "y": 2440.52, "z": 65.61}, {"x": 5156.21, "y": 2439.65, "z": 65.64}, {"x": 5155.81, "y": 2438.77, "z": 65.67}, {"x": 5155.45, "y": 2437.87, "z": 65.69}, {"x": 5155.12, "y": 2436.96, "z": 65.71}, {"x": 5154.82, "y": 2436.03, "z": 65.75}, {"x": 5154.56, "y": 2435.1, "z": 65.77}, {"x": 5154.35, "y": 2434.17, "z": 65.79}, {"x": 5154.17, "y": 2433.23, "z": 65.81}, {"x": 5154.05, "y": 2432.3, "z": 65.83}, {"x": 5153.97, "y": 2431.37, "z": 65.84}, {"x": 5153.94, "y": 2430.45, "z": 65.85}, {"x": 5153.97, "y": 2429.54, "z": 65.86}, {"x": 5154.05, "y": 2428.64, "z": 65.86}, {"x": 5154.2, "y": 2427.75, "z": 65.87}, {"x": 5154.41, "y": 2426.89, "z": 65.88}, {"x": 5154.68, "y": 2426.05, "z": 65.9}, {"x": 5155.02, "y": 2425.23, "z": 65.9}, {"x": 5155.43, "y": 2424.44, "z": 65.91}, {"x": 5155.91, "y": 2423.68, "z": 65.93}, {"x": 5156.47, "y": 2422.95, "z": 65.95}, {"x": 5157.1, "y": 2422.26, "z": 66.02}, {"x": 5157.17, "y": 2422.2, "z": 66.01}, {"x": 5160.51, "y": 2419.95, "z": 66.15}], "right_lane_mark_type": "NONE", "successors": [38133154, 38133155], "predecessors": [38119951], "right_neighbor_id": null, "left_neighbor_id": null}, "38111880": {"id": 38111880, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.03, "y": 2426.94, "z": 66.29}, {"x": 5165.15, "y": 2427.4, "z": 66.26}, {"x": 5164.25, "y": 2427.72, "z": 66.23}, {"x": 5163.35, "y": 2427.9, "z": 66.2}, {"x": 5162.43, "y": 2427.96, "z": 66.18}, {"x": 5161.52, "y": 2427.91, "z": 66.15}, {"x": 5160.62, "y": 2427.75, "z": 66.11}, {"x": 5159.73, "y": 2427.49, "z": 66.08}, {"x": 5158.85, "y": 2427.14, "z": 66.06}, {"x": 5158.0, "y": 2426.71, "z": 66.02}, {"x": 5157.18, "y": 2426.22, "z": 65.99}, {"x": 5156.39, "y": 2425.66, "z": 65.95}, {"x": 5155.64, "y": 2425.05, "z": 65.93}, {"x": 5154.94, "y": 2424.4, "z": 65.89}, {"x": 5154.28, "y": 2423.71, "z": 65.86}, {"x": 5153.69, "y": 2423.0, "z": 65.82}, {"x": 5153.16, "y": 2422.28, "z": 65.79}, {"x": 5152.69, "y": 2421.54, "z": 65.79}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5169.46, "y": 2431.57, "z": 66.22}, {"x": 5168.51, "y": 2432.24, "z": 66.18}, {"x": 5167.8, "y": 2432.68, "z": 66.15}, {"x": 5166.94, "y": 2433.12, "z": 66.04}, {"x": 5166.06, "y": 2433.5, "z": 66.03}, {"x": 5165.16, "y": 2433.8, "z": 65.99}, {"x": 5164.25, "y": 2434.04, "z": 65.97}, {"x": 5163.33, "y": 2434.22, "z": 65.96}, {"x": 5162.39, "y": 2434.33, "z": 65.95}, {"x": 5161.46, "y": 2434.37, "z": 65.94}, {"x": 5160.51, "y": 2434.36, "z": 65.93}, {"x": 5159.57, "y": 2434.29, "z": 65.91}, {"x": 5158.63, "y": 2434.17, "z": 65.89}, {"x": 5157.7, "y": 2433.99, "z": 65.88}, {"x": 5156.77, "y": 2433.76, "z": 65.86}, {"x": 5155.85, "y": 2433.48, "z": 65.85}, {"x": 5154.95, "y": 2433.15, "z": 65.84}, {"x": 5154.05, "y": 2432.78, "z": 65.81}, {"x": 5153.18, "y": 2432.36, "z": 65.8}, {"x": 5152.33, "y": 2431.9, "z": 65.78}, {"x": 5151.5, "y": 2431.39, "z": 65.76}, {"x": 5150.7, "y": 2430.85, "z": 65.74}, {"x": 5149.93, "y": 2430.27, "z": 65.71}, {"x": 5149.18, "y": 2429.65, "z": 65.68}, {"x": 5148.47, "y": 2429.01, "z": 65.65}, {"x": 5147.8, "y": 2428.33, "z": 65.63}, {"x": 5147.17, "y": 2427.62, "z": 65.6}, {"x": 5146.58, "y": 2426.88, "z": 65.6}, {"x": 5146.08, "y": 2426.08, "z": 65.61}], "right_lane_mark_type": "NONE", "successors": [38111342], "predecessors": [38111662], "right_neighbor_id": null, "left_neighbor_id": null}, "38111884": {"id": 38111884, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.37, "y": 2440.79, "z": 65.95}, {"x": 5165.73, "y": 2439.91, "z": 65.99}, {"x": 5164.94, "y": 2439.21, "z": 65.91}, {"x": 5164.14, "y": 2438.58, "z": 65.89}, {"x": 5163.33, "y": 2438.04, "z": 65.88}, {"x": 5162.5, "y": 2437.57, "z": 65.87}, {"x": 5161.66, "y": 2437.18, "z": 65.86}, {"x": 5160.82, "y": 2436.86, "z": 65.86}, {"x": 5159.96, "y": 2436.61, "z": 65.85}, {"x": 5159.11, "y": 2436.43, "z": 65.84}, {"x": 5158.25, "y": 2436.32, "z": 65.82}, {"x": 5157.39, "y": 2436.28, "z": 65.8}, {"x": 5156.54, "y": 2436.31, "z": 65.77}, {"x": 5155.69, "y": 2436.39, "z": 65.75}, {"x": 5154.86, "y": 2436.54, "z": 65.72}, {"x": 5154.03, "y": 2436.76, "z": 65.69}, {"x": 5153.21, "y": 2437.03, "z": 65.67}, {"x": 5152.41, "y": 2437.35, "z": 65.64}, {"x": 5151.62, "y": 2437.74, "z": 65.6}, {"x": 5150.86, "y": 2438.18, "z": 65.57}, {"x": 5150.12, "y": 2438.67, "z": 65.53}, {"x": 5149.4, "y": 2439.21, "z": 65.5}, {"x": 5148.71, "y": 2439.81, "z": 65.47}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5160.14, "y": 2445.81, "z": 65.74}, {"x": 5159.61, "y": 2444.88, "z": 65.7}, {"x": 5159.05, "y": 2444.09, "z": 65.66}, {"x": 5158.46, "y": 2443.44, "z": 65.64}, {"x": 5157.83, "y": 2442.93, "z": 65.6}, {"x": 5157.17, "y": 2442.54, "z": 65.56}, {"x": 5156.47, "y": 2442.27, "z": 65.54}, {"x": 5155.75, "y": 2442.11, "z": 65.52}, {"x": 5155.0, "y": 2442.06, "z": 65.5}, {"x": 5154.22, "y": 2442.1, "z": 65.49}, {"x": 5153.42, "y": 2442.24, "z": 65.47}, {"x": 5152.6, "y": 2442.46, "z": 65.44}, {"x": 5151.75, "y": 2442.76, "z": 65.41}, {"x": 5150.88, "y": 2443.13, "z": 65.39}], "right_lane_mark_type": "NONE", "successors": [38111173], "predecessors": [38119951], "right_neighbor_id": null, "left_neighbor_id": null}, "38111894": {"id": 38111894, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5288.86, "y": 2345.49, "z": 70.99}, {"x": 5272.94, "y": 2353.69, "z": 70.51}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5289.51, "y": 2346.62, "z": 70.98}, {"x": 5275.94, "y": 2358.54, "z": 70.49}], "right_lane_mark_type": "NONE", "successors": [38109382], "predecessors": [38111904], "right_neighbor_id": null, "left_neighbor_id": null}, "38111898": {"id": 38111898, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5135.52, "y": 2449.51, "z": 64.99}, {"x": 5131.81, "y": 2454.6, "z": 64.83}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5137.92, "y": 2452.76, "z": 64.93}, {"x": 5133.92, "y": 2458.31, "z": 64.74}], "right_lane_mark_type": "NONE", "successors": [38114770], "predecessors": [38111696], "right_neighbor_id": null, "left_neighbor_id": null}, "38111902": {"id": 38111902, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5332.07, "y": 2315.53, "z": 72.0}, {"x": 5312.04, "y": 2329.04, "z": 71.8}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5332.94, "y": 2316.73, "z": 71.94}, {"x": 5312.95, "y": 2330.28, "z": 71.79}], "right_lane_mark_type": "NONE", "successors": [38111905], "predecessors": [38111899], "right_neighbor_id": null, "left_neighbor_id": 38109397}, "38111904": {"id": 38111904, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5306.47, "y": 2332.86, "z": 71.62}, {"x": 5288.86, "y": 2345.49, "z": 70.99}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5307.33, "y": 2334.12, "z": 71.6}, {"x": 5289.51, "y": 2346.62, "z": 70.98}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111894, 38111937], "predecessors": [38111905], "right_neighbor_id": null, "left_neighbor_id": 38109234}, "38111905": {"id": 38111905, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5312.04, "y": 2329.04, "z": 71.8}, {"x": 5306.47, "y": 2332.86, "z": 71.62}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5312.95, "y": 2330.28, "z": 71.79}, {"x": 5308.42, "y": 2333.35, "z": 71.64}, {"x": 5307.33, "y": 2334.12, "z": 71.6}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111904], "predecessors": [38111902, 38111935], "right_neighbor_id": null, "left_neighbor_id": 38111133}, "38111935": {"id": 38111935, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5327.53, "y": 2329.17, "z": 72.01}, {"x": 5326.98, "y": 2328.13, "z": 72.01}, {"x": 5326.64, "y": 2327.62, "z": 72.01}, {"x": 5326.21, "y": 2327.26, "z": 71.98}, {"x": 5325.72, "y": 2326.92, "z": 71.97}, {"x": 5325.17, "y": 2326.6, "z": 71.96}, {"x": 5324.56, "y": 2326.31, "z": 71.96}, {"x": 5323.89, "y": 2326.06, "z": 71.97}, {"x": 5323.18, "y": 2325.86, "z": 71.99}, {"x": 5322.4, "y": 2325.71, "z": 71.99}, {"x": 5321.58, "y": 2325.63, "z": 71.98}, {"x": 5320.7, "y": 2325.62, "z": 71.97}, {"x": 5319.78, "y": 2325.68, "z": 71.97}, {"x": 5318.81, "y": 2325.84, "z": 71.97}, {"x": 5317.79, "y": 2326.09, "z": 71.96}, {"x": 5316.72, "y": 2326.44, "z": 71.94}, {"x": 5315.61, "y": 2326.9, "z": 71.91}, {"x": 5314.46, "y": 2327.48, "z": 71.88}, {"x": 5313.27, "y": 2328.19, "z": 71.84}, {"x": 5312.04, "y": 2329.04, "z": 71.8}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5320.76, "y": 2333.27, "z": 71.87}, {"x": 5319.86, "y": 2331.41, "z": 71.85}, {"x": 5319.14, "y": 2330.69, "z": 71.84}, {"x": 5318.37, "y": 2330.4, "z": 71.83}, {"x": 5317.58, "y": 2330.3, "z": 71.83}, {"x": 5315.46, "y": 2330.24, "z": 71.83}, {"x": 5314.48, "y": 2330.23, "z": 71.82}, {"x": 5313.62, "y": 2330.28, "z": 71.8}, {"x": 5312.95, "y": 2330.28, "z": 71.79}], "right_lane_mark_type": "NONE", "successors": [38111905], "predecessors": [38109176], "right_neighbor_id": null, "left_neighbor_id": null}, "38111937": {"id": 38111937, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5288.86, "y": 2345.49, "z": 70.99}, {"x": 5288.23, "y": 2345.66, "z": 70.98}, {"x": 5287.57, "y": 2345.63, "z": 70.97}, {"x": 5286.87, "y": 2345.42, "z": 70.96}, {"x": 5286.15, "y": 2345.04, "z": 70.96}, {"x": 5285.42, "y": 2344.54, "z": 70.97}, {"x": 5284.69, "y": 2343.92, "z": 70.98}, {"x": 5283.97, "y": 2343.21, "z": 70.97}, {"x": 5283.25, "y": 2342.44, "z": 70.96}, {"x": 5282.56, "y": 2341.63, "z": 70.95}, {"x": 5281.91, "y": 2340.8, "z": 70.94}, {"x": 5281.3, "y": 2339.97, "z": 70.92}, {"x": 5280.73, "y": 2339.17, "z": 70.9}, {"x": 5280.23, "y": 2338.43, "z": 70.87}, {"x": 5279.8, "y": 2337.75, "z": 70.88}, {"x": 5279.45, "y": 2337.18, "z": 70.9}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5289.51, "y": 2346.62, "z": 70.98}, {"x": 5289.27, "y": 2346.88, "z": 70.96}, {"x": 5288.96, "y": 2347.13, "z": 70.96}, {"x": 5288.59, "y": 2347.37, "z": 70.94}, {"x": 5288.16, "y": 2347.57, "z": 70.93}, {"x": 5287.66, "y": 2347.73, "z": 70.91}, {"x": 5287.11, "y": 2347.84, "z": 70.9}, {"x": 5286.5, "y": 2347.9, "z": 70.89}, {"x": 5285.83, "y": 2347.88, "z": 70.88}, {"x": 5285.1, "y": 2347.78, "z": 70.86}, {"x": 5284.32, "y": 2347.6, "z": 70.85}, {"x": 5283.48, "y": 2347.32, "z": 70.85}, {"x": 5282.59, "y": 2346.92, "z": 70.85}, {"x": 5281.65, "y": 2346.41, "z": 70.85}, {"x": 5280.67, "y": 2345.78, "z": 70.84}, {"x": 5279.63, "y": 2345.0, "z": 70.83}, {"x": 5278.54, "y": 2344.08, "z": 70.81}, {"x": 5277.41, "y": 2342.99, "z": 70.77}, {"x": 5276.23, "y": 2341.75, "z": 70.72}, {"x": 5275.01, "y": 2340.32, "z": 70.71}], "right_lane_mark_type": "NONE", "successors": [38109482], "predecessors": [38111904], "right_neighbor_id": null, "left_neighbor_id": null}, "38114309": {"id": 38114309, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5224.76, "y": 2328.95, "z": 70.43}, {"x": 5219.69, "y": 2332.21, "z": 70.08}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5226.56, "y": 2331.45, "z": 70.42}, {"x": 5221.28, "y": 2334.87, "z": 69.99}], "right_lane_mark_type": "NONE", "successors": [38114427, 38114403], "predecessors": [38115208], "right_neighbor_id": null, "left_neighbor_id": null}, "38114318": {"id": 38114318, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5230.74, "y": 2365.5, "z": 69.44}, {"x": 5232.17, "y": 2367.83, "z": 69.33}, {"x": 5232.45, "y": 2368.38, "z": 69.32}, {"x": 5232.61, "y": 2368.82, "z": 69.31}, {"x": 5232.75, "y": 2369.41, "z": 69.29}, {"x": 5232.82, "y": 2370.07, "z": 69.27}, {"x": 5232.86, "y": 2370.8, "z": 69.25}, {"x": 5232.88, "y": 2371.77, "z": 69.25}, {"x": 5232.87, "y": 2372.57, "z": 69.25}, {"x": 5232.8, "y": 2373.73, "z": 69.24}, {"x": 5232.74, "y": 2374.61, "z": 69.24}, {"x": 5232.68, "y": 2375.56, "z": 69.23}, {"x": 5232.56, "y": 2376.49, "z": 69.21}, {"x": 5232.39, "y": 2377.39, "z": 69.18}, {"x": 5232.17, "y": 2378.26, "z": 69.16}, {"x": 5231.9, "y": 2379.1, "z": 69.14}, {"x": 5231.59, "y": 2379.91, "z": 69.11}, {"x": 5231.24, "y": 2380.69, "z": 69.08}, {"x": 5230.84, "y": 2381.42, "z": 69.05}, {"x": 5230.41, "y": 2382.12, "z": 69.03}, {"x": 5229.93, "y": 2382.77, "z": 69.0}, {"x": 5229.42, "y": 2383.38, "z": 68.98}, {"x": 5228.87, "y": 2383.94, "z": 68.95}, {"x": 5228.29, "y": 2384.45, "z": 68.92}, {"x": 5227.04, "y": 2385.3, "z": 68.87}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5235.67, "y": 2362.4, "z": 69.55}, {"x": 5236.16, "y": 2363.28, "z": 69.51}, {"x": 5236.6, "y": 2364.18, "z": 69.49}, {"x": 5237.0, "y": 2365.1, "z": 69.44}, {"x": 5237.34, "y": 2366.04, "z": 69.43}, {"x": 5237.64, "y": 2367.0, "z": 69.41}, {"x": 5237.9, "y": 2367.97, "z": 69.41}, {"x": 5238.11, "y": 2368.94, "z": 69.42}, {"x": 5238.28, "y": 2369.93, "z": 69.43}, {"x": 5238.4, "y": 2370.92, "z": 69.43}, {"x": 5238.48, "y": 2371.92, "z": 69.43}, {"x": 5238.52, "y": 2372.91, "z": 69.42}, {"x": 5238.52, "y": 2373.91, "z": 69.4}, {"x": 5238.47, "y": 2374.9, "z": 69.38}, {"x": 5238.39, "y": 2375.88, "z": 69.37}, {"x": 5238.26, "y": 2376.86, "z": 69.34}, {"x": 5238.1, "y": 2377.82, "z": 69.3}, {"x": 5237.9, "y": 2378.77, "z": 69.27}, {"x": 5237.66, "y": 2379.7, "z": 69.24}, {"x": 5237.39, "y": 2380.62, "z": 69.2}, {"x": 5237.08, "y": 2381.51, "z": 69.16}, {"x": 5236.73, "y": 2382.38, "z": 69.12}, {"x": 5236.35, "y": 2383.22, "z": 69.09}, {"x": 5235.93, "y": 2384.04, "z": 69.05}, {"x": 5235.48, "y": 2384.82, "z": 69.02}, {"x": 5235.0, "y": 2385.57, "z": 68.98}, {"x": 5234.49, "y": 2386.29, "z": 68.95}, {"x": 5233.94, "y": 2386.97, "z": 68.91}, {"x": 5233.37, "y": 2387.6, "z": 68.88}, {"x": 5232.76, "y": 2388.2, "z": 68.85}, {"x": 5232.13, "y": 2388.75, "z": 68.84}, {"x": 5231.35, "y": 2389.25, "z": 68.83}, {"x": 5230.31, "y": 2389.95, "z": 68.79}, {"x": 5230.05, "y": 2390.09, "z": 68.78}], "right_lane_mark_type": "NONE", "successors": [38114436], "predecessors": [38114351], "right_neighbor_id": null, "left_neighbor_id": null}, "38114332": {"id": 38114332, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5237.88, "y": 2391.1, "z": 68.99}, {"x": 5244.23, "y": 2400.0, "z": 69.18}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5242.29, "y": 2387.75, "z": 69.16}, {"x": 5251.14, "y": 2400.0, "z": 69.24}], "right_lane_mark_type": "NONE", "successors": [38109824], "predecessors": [38114376, 38114428, 38114405], "right_neighbor_id": null, "left_neighbor_id": null}, "38114334": {"id": 38114334, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5221.27, "y": 2334.88, "z": 69.99}, {"x": 5221.91, "y": 2334.46, "z": 70.04}, {"x": 5226.56, "y": 2331.45, "z": 70.42}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5219.7, "y": 2332.2, "z": 70.08}, {"x": 5224.76, "y": 2328.95, "z": 70.43}], "right_lane_mark_type": "NONE", "successors": [38115671], "predecessors": [38114390, 38114407], "right_neighbor_id": null, "left_neighbor_id": null}, "38114335": {"id": 38114335, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5208.47, "y": 2336.18, "z": 69.85}, {"x": 5204.79, "y": 2338.45, "z": 69.67}, {"x": 5174.16, "y": 2356.73, "z": 67.63}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5210.02, "y": 2338.55, "z": 69.89}, {"x": 5191.39, "y": 2349.92, "z": 68.75}, {"x": 5175.92, "y": 2359.61, "z": 67.7}], "right_lane_mark_type": "NONE", "successors": [38120769, 38119984], "predecessors": [38114427, 38114348], "right_neighbor_id": null, "left_neighbor_id": null}, "38114336": {"id": 38114336, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5154.86, "y": 2370.0, "z": 66.34}, {"x": 5167.66, "y": 2362.35, "z": 67.19}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5148.63, "y": 2370.0, "z": 66.08}, {"x": 5166.06, "y": 2359.67, "z": 67.17}], "right_lane_mark_type": "NONE", "successors": [38120167, 38120430], "predecessors": [38119868], "right_neighbor_id": null, "left_neighbor_id": null}, "38114340": {"id": 38114340, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5245.56, "y": 2372.45, "z": 69.59}, {"x": 5227.04, "y": 2385.3, "z": 68.87}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5249.22, "y": 2376.88, "z": 69.58}, {"x": 5241.62, "y": 2382.07, "z": 69.22}, {"x": 5230.05, "y": 2390.09, "z": 68.78}], "right_lane_mark_type": "NONE", "successors": [38114436], "predecessors": [38116085], "right_neighbor_id": null, "left_neighbor_id": null}, "38114347": {"id": 38114347, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5208.92, "y": 2329.99, "z": 69.95}, {"x": 5210.11, "y": 2331.86, "z": 69.9}, {"x": 5214.63, "y": 2339.32, "z": 69.86}, {"x": 5215.0, "y": 2339.91, "z": 69.83}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5213.9, "y": 2326.93, "z": 70.07}, {"x": 5217.01, "y": 2332.02, "z": 70.01}, {"x": 5220.1, "y": 2337.01, "z": 69.89}], "right_lane_mark_type": "NONE", "successors": [38114351], "predecessors": [38114410], "right_neighbor_id": null, "left_neighbor_id": null}, "38114348": {"id": 38114348, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5208.92, "y": 2329.99, "z": 69.95}, {"x": 5209.32, "y": 2330.64, "z": 69.94}, {"x": 5209.46, "y": 2330.95, "z": 69.93}, {"x": 5209.62, "y": 2331.23, "z": 69.92}, {"x": 5209.98, "y": 2332.34, "z": 69.89}, {"x": 5209.92, "y": 2334.19, "z": 69.85}, {"x": 5209.62, "y": 2334.91, "z": 69.85}, {"x": 5209.24, "y": 2335.48, "z": 69.86}, {"x": 5208.83, "y": 2335.91, "z": 69.86}, {"x": 5208.47, "y": 2336.18, "z": 69.85}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5213.9, "y": 2326.93, "z": 70.07}, {"x": 5214.04, "y": 2327.15, "z": 70.07}, {"x": 5214.15, "y": 2327.34, "z": 70.07}, {"x": 5214.54, "y": 2328.11, "z": 70.06}, {"x": 5214.92, "y": 2329.3, "z": 70.03}, {"x": 5215.08, "y": 2330.47, "z": 70.01}, {"x": 5215.2, "y": 2331.54, "z": 70.0}, {"x": 5214.88, "y": 2332.67, "z": 69.97}, {"x": 5214.58, "y": 2333.69, "z": 69.94}, {"x": 5214.16, "y": 2334.64, "z": 69.9}, {"x": 5213.66, "y": 2335.51, "z": 69.87}, {"x": 5213.11, "y": 2336.29, "z": 69.84}, {"x": 5212.53, "y": 2336.98, "z": 69.83}, {"x": 5212.14, "y": 2337.35, "z": 69.84}, {"x": 5211.67, "y": 2337.71, "z": 69.86}, {"x": 5211.18, "y": 2337.96, "z": 69.9}, {"x": 5210.95, "y": 2338.04, "z": 69.9}, {"x": 5210.4, "y": 2338.36, "z": 69.9}, {"x": 5210.02, "y": 2338.55, "z": 69.89}], "right_lane_mark_type": "NONE", "successors": [38114335], "predecessors": [38114410], "right_neighbor_id": null, "left_neighbor_id": null}, "38114349": {"id": 38114349, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5220.0, "y": 2390.12, "z": 68.56}, {"x": 5227.04, "y": 2385.3, "z": 68.87}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5220.0, "y": 2386.47, "z": 68.63}, {"x": 5225.39, "y": 2382.69, "z": 68.88}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38114428], "predecessors": [38114426], "right_neighbor_id": 38114404, "left_neighbor_id": 38114436}, "38114351": {"id": 38114351, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5215.0, "y": 2339.91, "z": 69.83}, {"x": 5230.74, "y": 2365.5, "z": 69.44}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5220.1, "y": 2337.01, "z": 69.89}, {"x": 5235.67, "y": 2362.4, "z": 69.55}], "right_lane_mark_type": "NONE", "successors": [38114318, 38114446, 38114405], "predecessors": [38114355, 38114347, 38114403], "right_neighbor_id": null, "left_neighbor_id": null}, "38114355": {"id": 38114355, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5209.29, "y": 2337.42, "z": 69.87}, {"x": 5209.76, "y": 2337.16, "z": 69.87}, {"x": 5210.33, "y": 2337.03, "z": 69.87}, {"x": 5210.93, "y": 2337.04, "z": 69.86}, {"x": 5211.71, "y": 2337.17, "z": 69.84}, {"x": 5212.46, "y": 2337.38, "z": 69.84}, {"x": 5213.05, "y": 2337.67, "z": 69.84}, {"x": 5213.68, "y": 2338.15, "z": 69.85}, {"x": 5214.2, "y": 2338.64, "z": 69.87}, {"x": 5214.63, "y": 2339.32, "z": 69.86}, {"x": 5215.0, "y": 2339.91, "z": 69.83}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5207.69, "y": 2334.98, "z": 69.87}, {"x": 5208.41, "y": 2334.54, "z": 69.87}, {"x": 5209.04, "y": 2334.21, "z": 69.86}, {"x": 5209.82, "y": 2333.91, "z": 69.85}, {"x": 5210.65, "y": 2333.66, "z": 69.86}, {"x": 5211.86, "y": 2333.5, "z": 69.89}, {"x": 5213.16, "y": 2333.47, "z": 69.91}, {"x": 5214.42, "y": 2333.63, "z": 69.94}, {"x": 5215.47, "y": 2333.86, "z": 69.95}, {"x": 5216.42, "y": 2334.17, "z": 69.95}, {"x": 5217.5, "y": 2334.66, "z": 69.94}, {"x": 5218.4, "y": 2335.31, "z": 69.92}, {"x": 5219.28, "y": 2336.07, "z": 69.9}, {"x": 5220.1, "y": 2337.01, "z": 69.89}], "right_lane_mark_type": "NONE", "successors": [38114351], "predecessors": [38120064], "right_neighbor_id": null, "left_neighbor_id": null}, "38114374": {"id": 38114374, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5225.39, "y": 2382.69, "z": 68.88}, {"x": 5244.11, "y": 2370.36, "z": 69.6}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5222.35, "y": 2377.97, "z": 68.82}, {"x": 5240.78, "y": 2365.26, "z": 69.5}], "right_lane_mark_type": "NONE", "successors": [38109359], "predecessors": [38114404], "right_neighbor_id": null, "left_neighbor_id": null}, "38114376": {"id": 38114376, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5245.56, "y": 2372.45, "z": 69.59}, {"x": 5244.29, "y": 2373.34, "z": 69.55}, {"x": 5243.1, "y": 2374.18, "z": 69.5}, {"x": 5241.99, "y": 2374.97, "z": 69.46}, {"x": 5240.96, "y": 2375.73, "z": 69.42}, {"x": 5240.01, "y": 2376.45, "z": 69.39}, {"x": 5239.15, "y": 2377.14, "z": 69.35}, {"x": 5238.36, "y": 2377.8, "z": 69.31}, {"x": 5237.65, "y": 2378.44, "z": 69.28}, {"x": 5237.02, "y": 2379.06, "z": 69.25}, {"x": 5236.46, "y": 2379.66, "z": 69.22}, {"x": 5235.99, "y": 2380.26, "z": 69.19}, {"x": 5235.59, "y": 2380.85, "z": 69.16}, {"x": 5235.27, "y": 2381.45, "z": 69.14}, {"x": 5235.02, "y": 2382.04, "z": 69.12}, {"x": 5234.85, "y": 2382.65, "z": 69.09}, {"x": 5234.75, "y": 2383.27, "z": 69.07}, {"x": 5234.73, "y": 2383.9, "z": 69.05}, {"x": 5234.79, "y": 2384.56, "z": 69.02}, {"x": 5234.91, "y": 2385.25, "z": 69.0}, {"x": 5235.11, "y": 2385.96, "z": 68.97}, {"x": 5235.38, "y": 2386.71, "z": 68.95}, {"x": 5235.73, "y": 2387.5, "z": 68.93}, {"x": 5236.14, "y": 2388.34, "z": 68.93}, {"x": 5236.63, "y": 2389.22, "z": 68.94}, {"x": 5237.18, "y": 2390.16, "z": 68.95}, {"x": 5237.88, "y": 2391.1, "z": 68.99}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5249.22, "y": 2376.88, "z": 69.58}, {"x": 5244.85, "y": 2379.86, "z": 69.36}, {"x": 5242.02, "y": 2381.8, "z": 69.25}, {"x": 5241.62, "y": 2382.07, "z": 69.22}, {"x": 5241.07, "y": 2382.56, "z": 69.16}, {"x": 5240.75, "y": 2383.04, "z": 69.14}, {"x": 5240.58, "y": 2383.52, "z": 69.12}, {"x": 5240.48, "y": 2384.4, "z": 69.11}, {"x": 5240.58, "y": 2385.05, "z": 69.13}, {"x": 5240.81, "y": 2385.58, "z": 69.12}, {"x": 5241.24, "y": 2386.18, "z": 69.13}, {"x": 5242.29, "y": 2387.75, "z": 69.16}], "right_lane_mark_type": "NONE", "successors": [38114332], "predecessors": [38116085], "right_neighbor_id": null, "left_neighbor_id": null}, "38114390": {"id": 38114390, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5208.92, "y": 2329.99, "z": 69.95}, {"x": 5209.85, "y": 2331.47, "z": 69.91}, {"x": 5210.73, "y": 2332.74, "z": 69.88}, {"x": 5211.56, "y": 2333.82, "z": 69.87}, {"x": 5212.34, "y": 2334.72, "z": 69.87}, {"x": 5213.08, "y": 2335.45, "z": 69.86}, {"x": 5213.79, "y": 2336.01, "z": 69.87}, {"x": 5214.46, "y": 2336.44, "z": 69.87}, {"x": 5215.11, "y": 2336.73, "z": 69.87}, {"x": 5215.74, "y": 2336.9, "z": 69.89}, {"x": 5216.35, "y": 2336.96, "z": 69.9}, {"x": 5216.95, "y": 2336.93, "z": 69.91}, {"x": 5217.55, "y": 2336.81, "z": 69.91}, {"x": 5218.15, "y": 2336.61, "z": 69.9}, {"x": 5218.75, "y": 2336.36, "z": 69.9}, {"x": 5219.37, "y": 2336.06, "z": 69.9}, {"x": 5219.99, "y": 2335.69, "z": 69.91}, {"x": 5221.27, "y": 2334.88, "z": 69.99}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5213.9, "y": 2326.93, "z": 70.07}, {"x": 5217.0, "y": 2332.01, "z": 70.01}, {"x": 5217.42, "y": 2332.58, "z": 70.0}, {"x": 5217.74, "y": 2332.81, "z": 69.99}, {"x": 5218.02, "y": 2332.89, "z": 69.98}, {"x": 5218.35, "y": 2332.87, "z": 70.0}, {"x": 5218.66, "y": 2332.78, "z": 70.01}, {"x": 5219.7, "y": 2332.2, "z": 70.08}], "right_lane_mark_type": "NONE", "successors": [38114334], "predecessors": [38114410], "right_neighbor_id": null, "left_neighbor_id": null}, "38114403": {"id": 38114403, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5219.69, "y": 2332.21, "z": 70.08}, {"x": 5218.66, "y": 2332.78, "z": 70.01}, {"x": 5216.8, "y": 2333.81, "z": 69.96}, {"x": 5215.83, "y": 2334.63, "z": 69.94}, {"x": 5215.17, "y": 2335.47, "z": 69.91}, {"x": 5214.75, "y": 2336.29, "z": 69.88}, {"x": 5214.53, "y": 2337.08, "z": 69.86}, {"x": 5214.46, "y": 2337.8, "z": 69.84}, {"x": 5214.5, "y": 2338.45, "z": 69.85}, {"x": 5214.61, "y": 2339.0, "z": 69.84}, {"x": 5214.75, "y": 2339.42, "z": 69.85}, {"x": 5215.0, "y": 2339.91, "z": 69.83}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5221.28, "y": 2334.87, "z": 69.99}, {"x": 5220.03, "y": 2335.68, "z": 69.91}, {"x": 5219.78, "y": 2335.92, "z": 69.9}, {"x": 5219.71, "y": 2336.17, "z": 69.9}, {"x": 5219.79, "y": 2336.46, "z": 69.89}, {"x": 5220.1, "y": 2337.01, "z": 69.89}], "right_lane_mark_type": "NONE", "successors": [38114351], "predecessors": [38114309], "right_neighbor_id": null, "left_neighbor_id": null}, "38114404": {"id": 38114404, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5220.0, "y": 2386.47, "z": 68.63}, {"x": 5225.39, "y": 2382.69, "z": 68.88}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5220.0, "y": 2379.6, "z": 68.7}, {"x": 5222.35, "y": 2377.97, "z": 68.82}], "right_lane_mark_type": "NONE", "successors": [38114374], "predecessors": [38114433], "right_neighbor_id": null, "left_neighbor_id": 38114349}, "38114405": {"id": 38114405, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5230.74, "y": 2365.5, "z": 69.44}, {"x": 5233.22, "y": 2369.26, "z": 69.29}, {"x": 5233.47, "y": 2369.75, "z": 69.29}, {"x": 5233.61, "y": 2370.29, "z": 69.28}, {"x": 5233.69, "y": 2371.03, "z": 69.29}, {"x": 5234.7, "y": 2383.75, "z": 69.05}, {"x": 5234.82, "y": 2384.84, "z": 69.01}, {"x": 5235.03, "y": 2385.74, "z": 68.98}, {"x": 5235.36, "y": 2386.65, "z": 68.95}, {"x": 5235.79, "y": 2387.65, "z": 68.93}, {"x": 5236.25, "y": 2388.52, "z": 68.93}, {"x": 5236.71, "y": 2389.36, "z": 68.94}, {"x": 5237.29, "y": 2390.3, "z": 68.96}, {"x": 5237.88, "y": 2391.1, "z": 68.99}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5235.67, "y": 2362.4, "z": 69.55}, {"x": 5237.11, "y": 2364.92, "z": 69.44}, {"x": 5237.62, "y": 2366.33, "z": 69.42}, {"x": 5237.85, "y": 2367.31, "z": 69.41}, {"x": 5238.16, "y": 2369.27, "z": 69.43}, {"x": 5239.18, "y": 2382.96, "z": 69.1}, {"x": 5239.38, "y": 2384.02, "z": 69.07}, {"x": 5239.78, "y": 2384.76, "z": 69.07}, {"x": 5240.58, "y": 2385.72, "z": 69.1}, {"x": 5242.29, "y": 2387.75, "z": 69.16}], "right_lane_mark_type": "NONE", "successors": [38114332], "predecessors": [38114351], "right_neighbor_id": null, "left_neighbor_id": null}, "38114407": {"id": 38114407, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5209.29, "y": 2337.42, "z": 69.87}, {"x": 5210.4, "y": 2337.07, "z": 69.87}, {"x": 5219.42, "y": 2335.73, "z": 69.91}, {"x": 5221.27, "y": 2334.88, "z": 69.99}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5207.69, "y": 2334.98, "z": 69.87}, {"x": 5209.21, "y": 2334.34, "z": 69.85}, {"x": 5218.35, "y": 2332.87, "z": 70.0}, {"x": 5218.66, "y": 2332.78, "z": 70.01}, {"x": 5219.7, "y": 2332.2, "z": 70.08}], "right_lane_mark_type": "NONE", "successors": [38114334], "predecessors": [38120064], "right_neighbor_id": null, "left_neighbor_id": null}, "38114410": {"id": 38114410, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5190.0, "y": 2298.9, "z": 70.41}, {"x": 5192.4, "y": 2302.84, "z": 70.36}, {"x": 5193.57, "y": 2304.68, "z": 70.33}, {"x": 5208.92, "y": 2329.99, "z": 69.95}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5190.0, "y": 2287.69, "z": 70.64}, {"x": 5200.5, "y": 2304.89, "z": 70.42}, {"x": 5204.25, "y": 2311.16, "z": 70.31}, {"x": 5213.9, "y": 2326.93, "z": 70.07}], "right_lane_mark_type": "NONE", "successors": [38114347, 38114348, 38114390], "predecessors": [38114630], "right_neighbor_id": null, "left_neighbor_id": null}, "38114426": {"id": 38114426, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5205.75, "y": 2399.69, "z": 67.93}, {"x": 5220.0, "y": 2390.12, "z": 68.56}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5203.94, "y": 2397.25, "z": 67.92}, {"x": 5220.0, "y": 2386.47, "z": 68.63}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38114349], "predecessors": [38133156], "right_neighbor_id": 38114433, "left_neighbor_id": 38114432}, "38114427": {"id": 38114427, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5219.69, "y": 2332.21, "z": 70.08}, {"x": 5218.66, "y": 2332.78, "z": 70.01}, {"x": 5218.35, "y": 2332.87, "z": 70.0}, {"x": 5218.02, "y": 2332.89, "z": 69.98}, {"x": 5208.47, "y": 2336.18, "z": 69.85}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5221.28, "y": 2334.87, "z": 69.99}, {"x": 5219.68, "y": 2335.87, "z": 69.9}, {"x": 5211.69, "y": 2337.78, "z": 69.87}, {"x": 5210.95, "y": 2338.03, "z": 69.9}, {"x": 5210.02, "y": 2338.55, "z": 69.89}], "right_lane_mark_type": "NONE", "successors": [38114335], "predecessors": [38114309], "right_neighbor_id": null, "left_neighbor_id": null}, "38114428": {"id": 38114428, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5227.04, "y": 2385.3, "z": 68.87}, {"x": 5228.12, "y": 2384.9, "z": 68.9}, {"x": 5229.17, "y": 2384.73, "z": 68.94}, {"x": 5230.17, "y": 2384.75, "z": 68.96}, {"x": 5231.12, "y": 2384.94, "z": 68.98}, {"x": 5232.02, "y": 2385.28, "z": 68.98}, {"x": 5232.87, "y": 2385.74, "z": 68.96}, {"x": 5233.66, "y": 2386.3, "z": 68.94}, {"x": 5234.4, "y": 2386.93, "z": 68.92}, {"x": 5235.08, "y": 2387.6, "z": 68.91}, {"x": 5235.7, "y": 2388.29, "z": 68.91}, {"x": 5236.25, "y": 2388.98, "z": 68.91}, {"x": 5236.75, "y": 2389.63, "z": 68.94}, {"x": 5237.88, "y": 2391.1, "z": 68.99}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5225.39, "y": 2382.69, "z": 68.88}, {"x": 5225.83, "y": 2382.44, "z": 68.9}, {"x": 5226.15, "y": 2382.24, "z": 68.91}, {"x": 5226.55, "y": 2382.01, "z": 68.93}, {"x": 5227.01, "y": 2381.77, "z": 68.94}, {"x": 5227.55, "y": 2381.53, "z": 68.96}, {"x": 5228.14, "y": 2381.3, "z": 68.98}, {"x": 5228.8, "y": 2381.1, "z": 69.01}, {"x": 5229.53, "y": 2380.95, "z": 69.03}, {"x": 5230.31, "y": 2380.85, "z": 69.05}, {"x": 5231.15, "y": 2380.83, "z": 69.07}, {"x": 5232.05, "y": 2380.89, "z": 69.09}, {"x": 5233.0, "y": 2381.05, "z": 69.11}, {"x": 5234.0, "y": 2381.33, "z": 69.13}, {"x": 5235.05, "y": 2381.74, "z": 69.12}, {"x": 5236.16, "y": 2382.29, "z": 69.12}, {"x": 5237.31, "y": 2383.0, "z": 69.1}, {"x": 5238.5, "y": 2383.88, "z": 69.06}, {"x": 5239.74, "y": 2384.95, "z": 69.07}, {"x": 5241.02, "y": 2386.22, "z": 69.11}, {"x": 5242.29, "y": 2387.75, "z": 69.16}], "right_lane_mark_type": "NONE", "successors": [38114332], "predecessors": [38114349], "right_neighbor_id": null, "left_neighbor_id": null}, "38114432": {"id": 38114432, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5220.0, "y": 2390.12, "z": 68.56}, {"x": 5205.75, "y": 2399.69, "z": 67.93}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5220.0, "y": 2396.9, "z": 68.31}, {"x": 5215.51, "y": 2399.98, "z": 68.12}, {"x": 5209.27, "y": 2404.3, "z": 67.86}], "right_lane_mark_type": "NONE", "successors": [38110982], "predecessors": [38114436], "right_neighbor_id": null, "left_neighbor_id": 38114426}, "38114433": {"id": 38114433, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5203.94, "y": 2397.25, "z": 67.92}, {"x": 5220.0, "y": 2386.47, "z": 68.63}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5200.63, "y": 2392.74, "z": 67.86}, {"x": 5202.25, "y": 2391.63, "z": 67.93}, {"x": 5220.0, "y": 2379.6, "z": 68.7}], "right_lane_mark_type": "NONE", "successors": [38114404], "predecessors": [38133153], "right_neighbor_id": null, "left_neighbor_id": 38114426}, "38114436": {"id": 38114436, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5227.04, "y": 2385.3, "z": 68.87}, {"x": 5220.0, "y": 2390.12, "z": 68.56}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5230.05, "y": 2390.09, "z": 68.78}, {"x": 5228.92, "y": 2390.89, "z": 68.76}, {"x": 5220.0, "y": 2396.9, "z": 68.31}], "right_lane_mark_type": "NONE", "successors": [38114432], "predecessors": [38114340, 38114318], "right_neighbor_id": null, "left_neighbor_id": 38114349}, "38114446": {"id": 38114446, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5230.74, "y": 2365.5, "z": 69.44}, {"x": 5231.93, "y": 2367.44, "z": 69.35}, {"x": 5232.28, "y": 2367.88, "z": 69.33}, {"x": 5232.93, "y": 2368.58, "z": 69.31}, {"x": 5233.63, "y": 2369.21, "z": 69.3}, {"x": 5234.38, "y": 2369.77, "z": 69.31}, {"x": 5235.18, "y": 2370.27, "z": 69.34}, {"x": 5236.02, "y": 2370.7, "z": 69.36}, {"x": 5236.88, "y": 2371.04, "z": 69.39}, {"x": 5237.77, "y": 2371.3, "z": 69.41}, {"x": 5238.67, "y": 2371.47, "z": 69.44}, {"x": 5239.59, "y": 2371.55, "z": 69.46}, {"x": 5240.51, "y": 2371.53, "z": 69.48}, {"x": 5241.43, "y": 2371.41, "z": 69.51}, {"x": 5242.34, "y": 2371.17, "z": 69.54}, {"x": 5243.24, "y": 2370.83, "z": 69.57}, {"x": 5244.11, "y": 2370.36, "z": 69.6}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5235.67, "y": 2362.4, "z": 69.55}, {"x": 5236.44, "y": 2363.66, "z": 69.51}, {"x": 5236.83, "y": 2364.17, "z": 69.5}, {"x": 5237.2, "y": 2364.59, "z": 69.48}, {"x": 5237.57, "y": 2364.97, "z": 69.46}, {"x": 5238.03, "y": 2365.31, "z": 69.46}, {"x": 5238.44, "y": 2365.5, "z": 69.45}, {"x": 5238.94, "y": 2365.61, "z": 69.46}, {"x": 5239.44, "y": 2365.66, "z": 69.47}, {"x": 5240.0, "y": 2365.59, "z": 69.48}, {"x": 5240.68, "y": 2365.32, "z": 69.5}, {"x": 5240.78, "y": 2365.26, "z": 69.5}], "right_lane_mark_type": "NONE", "successors": [38109359], "predecessors": [38114351], "right_neighbor_id": null, "left_neighbor_id": null}, "38114630": {"id": 38114630, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5159.3, "y": 2248.4, "z": 71.45}, {"x": 5190.0, "y": 2298.9, "z": 70.41}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5164.32, "y": 2245.34, "z": 71.5}, {"x": 5166.74, "y": 2249.34, "z": 71.42}, {"x": 5171.2, "y": 2256.79, "z": 71.25}, {"x": 5180.6, "y": 2272.37, "z": 70.94}, {"x": 5188.46, "y": 2285.2, "z": 70.69}, {"x": 5190.0, "y": 2287.69, "z": 70.64}], "right_lane_mark_type": "NONE", "successors": [38114410], "predecessors": [38114695, 38114661, 38114679], "right_neighbor_id": null, "left_neighbor_id": null}, "38114654": {"id": 38114654, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5269.47, "y": 2420.63, "z": 69.66}, {"x": 5280.0, "y": 2413.56, "z": 70.11}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5266.45, "y": 2416.22, "z": 69.64}, {"x": 5266.77, "y": 2416.01, "z": 69.66}, {"x": 5267.88, "y": 2415.46, "z": 69.69}, {"x": 5280.0, "y": 2407.51, "z": 70.05}], "right_lane_mark_type": "NONE", "successors": [38109262], "predecessors": [38114664, 38114712], "right_neighbor_id": null, "left_neighbor_id": null}, "38114664": {"id": 38114664, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5259.1, "y": 2427.54, "z": 69.6}, {"x": 5268.01, "y": 2421.53, "z": 69.59}, {"x": 5269.47, "y": 2420.63, "z": 69.66}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5256.18, "y": 2423.59, "z": 69.51}, {"x": 5266.45, "y": 2416.22, "z": 69.64}], "right_lane_mark_type": "NONE", "successors": [38114654], "predecessors": [38116264], "right_neighbor_id": null, "left_neighbor_id": null}, "38114672": {"id": 38114672, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5257.14, "y": 2419.46, "z": 69.48}, {"x": 5262.76, "y": 2428.14, "z": 69.58}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5262.48, "y": 2415.83, "z": 69.51}, {"x": 5268.51, "y": 2424.22, "z": 69.62}], "right_lane_mark_type": "NONE", "successors": [38114694], "predecessors": [38109824], "right_neighbor_id": null, "left_neighbor_id": null}, "38114673": {"id": 38114673, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5259.1, "y": 2427.54, "z": 69.6}, {"x": 5260.03, "y": 2426.82, "z": 69.52}, {"x": 5260.36, "y": 2426.74, "z": 69.53}, {"x": 5260.44, "y": 2426.68, "z": 69.53}, {"x": 5260.71, "y": 2426.65, "z": 69.54}, {"x": 5260.87, "y": 2426.62, "z": 69.54}, {"x": 5261.59, "y": 2426.8, "z": 69.55}, {"x": 5262.17, "y": 2427.21, "z": 69.57}, {"x": 5262.57, "y": 2427.7, "z": 69.58}, {"x": 5262.76, "y": 2428.14, "z": 69.58}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5256.18, "y": 2423.59, "z": 69.51}, {"x": 5259.45, "y": 2421.38, "z": 69.52}, {"x": 5260.31, "y": 2421.16, "z": 69.53}, {"x": 5261.23, "y": 2421.02, "z": 69.54}, {"x": 5262.18, "y": 2420.97, "z": 69.53}, {"x": 5263.16, "y": 2421.02, "z": 69.53}, {"x": 5264.14, "y": 2421.19, "z": 69.53}, {"x": 5265.1, "y": 2421.49, "z": 69.53}, {"x": 5266.04, "y": 2421.92, "z": 69.53}, {"x": 5266.93, "y": 2422.52, "z": 69.54}, {"x": 5267.76, "y": 2423.28, "z": 69.6}, {"x": 5268.51, "y": 2424.22, "z": 69.62}], "right_lane_mark_type": "NONE", "successors": [38114694], "predecessors": [38116264], "right_neighbor_id": null, "left_neighbor_id": null}, "38114694": {"id": 38114694, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5262.76, "y": 2428.14, "z": 69.58}, {"x": 5268.61, "y": 2437.19, "z": 69.76}, {"x": 5305.05, "y": 2487.92, "z": 69.94}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5268.51, "y": 2424.22, "z": 69.62}, {"x": 5311.05, "y": 2483.62, "z": 69.91}], "right_lane_mark_type": "NONE", "successors": [38114658, 38114708, 38114688], "predecessors": [38114672, 38114673], "right_neighbor_id": null, "left_neighbor_id": null}, "38114698": {"id": 38114698, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5220.0, "y": 2454.47, "z": 67.99}, {"x": 5222.08, "y": 2453.05, "z": 68.15}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5220.0, "y": 2448.31, "z": 68.12}, {"x": 5220.21, "y": 2448.15, "z": 68.12}], "right_lane_mark_type": "NONE", "successors": [38116233, 38116384], "predecessors": [38119413], "right_neighbor_id": null, "left_neighbor_id": null}, "38114712": {"id": 38114712, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5257.14, "y": 2419.46, "z": 69.48}, {"x": 5257.87, "y": 2420.33, "z": 69.49}, {"x": 5259.35, "y": 2421.67, "z": 69.52}, {"x": 5260.1, "y": 2422.16, "z": 69.53}, {"x": 5260.85, "y": 2422.54, "z": 69.54}, {"x": 5261.61, "y": 2422.81, "z": 69.55}, {"x": 5262.37, "y": 2422.97, "z": 69.56}, {"x": 5263.14, "y": 2423.04, "z": 69.57}, {"x": 5263.91, "y": 2423.01, "z": 69.56}, {"x": 5264.69, "y": 2422.9, "z": 69.55}, {"x": 5265.48, "y": 2422.7, "z": 69.55}, {"x": 5266.26, "y": 2422.42, "z": 69.54}, {"x": 5267.06, "y": 2422.07, "z": 69.53}, {"x": 5267.86, "y": 2421.65, "z": 69.58}, {"x": 5268.66, "y": 2421.17, "z": 69.61}, {"x": 5269.47, "y": 2420.63, "z": 69.66}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5262.48, "y": 2415.83, "z": 69.51}, {"x": 5262.79, "y": 2416.24, "z": 69.52}, {"x": 5263.17, "y": 2416.76, "z": 69.5}, {"x": 5263.51, "y": 2417.09, "z": 69.49}, {"x": 5264.04, "y": 2417.26, "z": 69.5}, {"x": 5264.56, "y": 2417.17, "z": 69.52}, {"x": 5266.45, "y": 2416.22, "z": 69.64}], "right_lane_mark_type": "NONE", "successors": [38114654], "predecessors": [38109824], "right_neighbor_id": null, "left_neighbor_id": null}, "38114770": {"id": 38114770, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5131.81, "y": 2454.6, "z": 64.83}, {"x": 5130.0, "y": 2456.9, "z": 64.75}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5133.92, "y": 2458.31, "z": 64.74}, {"x": 5130.0, "y": 2462.47, "z": 64.59}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111163], "predecessors": [38111898], "right_neighbor_id": null, "left_neighbor_id": null}, "38115008": {"id": 38115008, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5262.32, "y": 2309.59, "z": 71.59}, {"x": 5262.01, "y": 2309.22, "z": 71.6}, {"x": 5262.0, "y": 2308.87, "z": 71.61}, {"x": 5261.95, "y": 2308.72, "z": 71.61}, {"x": 5261.88, "y": 2308.24, "z": 71.64}, {"x": 5261.92, "y": 2308.15, "z": 71.65}, {"x": 5261.92, "y": 2307.71, "z": 71.66}, {"x": 5262.35, "y": 2307.28, "z": 71.72}, {"x": 5262.37, "y": 2307.24, "z": 71.72}, {"x": 5262.87, "y": 2306.91, "z": 71.77}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5258.49, "y": 2312.06, "z": 71.56}, {"x": 5258.06, "y": 2310.81, "z": 71.57}, {"x": 5258.05, "y": 2309.42, "z": 71.58}, {"x": 5258.4, "y": 2308.01, "z": 71.59}, {"x": 5259.01, "y": 2306.74, "z": 71.61}, {"x": 5259.82, "y": 2305.72, "z": 71.66}, {"x": 5260.49, "y": 2305.28, "z": 71.71}, {"x": 5260.63, "y": 2305.2, "z": 71.72}, {"x": 5261.25, "y": 2304.78, "z": 71.77}], "right_lane_mark_type": "NONE", "successors": [38116016], "predecessors": [38115599], "right_neighbor_id": null, "left_neighbor_id": null}, "38115184": {"id": 38115184, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5302.93, "y": 2400.3, "z": 70.56}, {"x": 5302.29, "y": 2399.4, "z": 70.56}, {"x": 5302.29, "y": 2399.35, "z": 70.56}, {"x": 5302.04, "y": 2398.98, "z": 70.56}, {"x": 5302.02, "y": 2398.79, "z": 70.56}, {"x": 5302.1, "y": 2398.58, "z": 70.57}, {"x": 5302.24, "y": 2398.41, "z": 70.58}, {"x": 5302.76, "y": 2398.06, "z": 70.61}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5299.36, "y": 2402.61, "z": 70.52}, {"x": 5298.71, "y": 2401.7, "z": 70.51}, {"x": 5298.1, "y": 2400.57, "z": 70.47}, {"x": 5297.81, "y": 2399.21, "z": 70.47}, {"x": 5297.82, "y": 2397.73, "z": 70.48}, {"x": 5298.16, "y": 2396.26, "z": 70.5}, {"x": 5298.84, "y": 2394.92, "z": 70.54}, {"x": 5299.9, "y": 2393.9, "z": 70.58}], "right_lane_mark_type": "NONE", "successors": [38116425], "predecessors": [38116473], "right_neighbor_id": null, "left_neighbor_id": null}, "38115208": {"id": 38115208, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5254.01, "y": 2309.47, "z": 71.63}, {"x": 5253.92, "y": 2309.64, "z": 71.63}, {"x": 5252.61, "y": 2311.09, "z": 71.59}, {"x": 5251.54, "y": 2312.09, "z": 71.57}, {"x": 5240.51, "y": 2319.7, "z": 71.41}, {"x": 5224.76, "y": 2328.95, "z": 70.43}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5256.05, "y": 2312.88, "z": 71.56}, {"x": 5226.56, "y": 2331.45, "z": 70.42}], "right_lane_mark_type": "NONE", "successors": [38114309], "predecessors": [38116651, 38116021], "right_neighbor_id": null, "left_neighbor_id": null}, "38115599": {"id": 38115599, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5274.25, "y": 2328.25, "z": 71.24}, {"x": 5268.13, "y": 2319.68, "z": 71.4}, {"x": 5268.02, "y": 2319.49, "z": 71.41}, {"x": 5262.6, "y": 2310.08, "z": 71.58}, {"x": 5262.53, "y": 2309.95, "z": 71.59}, {"x": 5262.43, "y": 2309.77, "z": 71.59}, {"x": 5262.32, "y": 2309.59, "z": 71.59}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5269.43, "y": 2331.55, "z": 71.14}, {"x": 5266.23, "y": 2325.27, "z": 71.34}, {"x": 5266.24, "y": 2325.24, "z": 71.34}, {"x": 5266.02, "y": 2324.61, "z": 71.35}, {"x": 5265.82, "y": 2324.46, "z": 71.35}, {"x": 5265.26, "y": 2323.58, "z": 71.37}, {"x": 5259.45, "y": 2314.46, "z": 71.54}, {"x": 5258.6, "y": 2312.24, "z": 71.56}, {"x": 5258.49, "y": 2312.06, "z": 71.56}], "right_lane_mark_type": "NONE", "successors": [38116375, 38116021, 38115008], "predecessors": [38109482], "right_neighbor_id": null, "left_neighbor_id": null}, "38115671": {"id": 38115671, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5226.56, "y": 2331.45, "z": 70.42}, {"x": 5256.07, "y": 2312.86, "z": 71.56}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5224.76, "y": 2328.95, "z": 70.43}, {"x": 5240.51, "y": 2319.7, "z": 71.41}, {"x": 5251.54, "y": 2312.09, "z": 71.57}, {"x": 5252.61, "y": 2311.09, "z": 71.59}, {"x": 5253.92, "y": 2309.64, "z": 71.63}, {"x": 5254.02, "y": 2309.45, "z": 71.64}], "right_lane_mark_type": "NONE", "successors": [38116338, 38116337], "predecessors": [38114334], "right_neighbor_id": null, "left_neighbor_id": null}, "38116016": {"id": 38116016, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5262.87, "y": 2306.91, "z": 71.77}, {"x": 5296.48, "y": 2284.83, "z": 72.77}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5261.25, "y": 2304.78, "z": 71.77}, {"x": 5294.99, "y": 2282.02, "z": 72.82}], "right_lane_mark_type": "NONE", "successors": [38114327], "predecessors": [38115008, 38116338], "right_neighbor_id": null, "left_neighbor_id": null}, "38116021": {"id": 38116021, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5262.32, "y": 2309.59, "z": 71.59}, {"x": 5261.23, "y": 2308.61, "z": 71.61}, {"x": 5259.78, "y": 2308.04, "z": 71.6}, {"x": 5258.15, "y": 2307.84, "z": 71.6}, {"x": 5256.53, "y": 2307.98, "z": 71.59}, {"x": 5255.19, "y": 2308.51, "z": 71.61}, {"x": 5254.01, "y": 2309.47, "z": 71.63}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5258.49, "y": 2312.06, "z": 71.56}, {"x": 5258.39, "y": 2311.9, "z": 71.56}, {"x": 5257.95, "y": 2311.81, "z": 71.56}, {"x": 5256.98, "y": 2312.29, "z": 71.58}, {"x": 5256.86, "y": 2312.36, "z": 71.57}, {"x": 5256.05, "y": 2312.88, "z": 71.56}], "right_lane_mark_type": "NONE", "successors": [38115208], "predecessors": [38115599], "right_neighbor_id": null, "left_neighbor_id": null}, "38116066": {"id": 38116066, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5089.91, "y": 2422.35, "z": 62.6}, {"x": 5122.49, "y": 2400.0, "z": 64.42}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5088.2, "y": 2420.16, "z": 62.58}, {"x": 5091.22, "y": 2418.13, "z": 62.78}, {"x": 5098.03, "y": 2413.46, "z": 63.16}, {"x": 5117.72, "y": 2400.0, "z": 64.3}], "right_lane_mark_type": "NONE", "successors": [38119874], "predecessors": [38116069], "right_neighbor_id": null, "left_neighbor_id": null}, "38116069": {"id": 38116069, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5082.04, "y": 2421.08, "z": 62.25}, {"x": 5083.05, "y": 2422.38, "z": 62.27}, {"x": 5084.13, "y": 2423.28, "z": 62.3}, {"x": 5085.31, "y": 2423.77, "z": 62.34}, {"x": 5086.57, "y": 2423.85, "z": 62.4}, {"x": 5087.94, "y": 2423.49, "z": 62.47}, {"x": 5089.42, "y": 2422.68, "z": 62.56}, {"x": 5089.91, "y": 2422.35, "z": 62.6}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5084.69, "y": 2420.0, "z": 62.3}, {"x": 5084.98, "y": 2420.46, "z": 62.31}, {"x": 5085.48, "y": 2421.06, "z": 62.34}, {"x": 5085.51, "y": 2421.09, "z": 62.35}, {"x": 5085.83, "y": 2421.18, "z": 62.36}, {"x": 5086.28, "y": 2421.23, "z": 62.4}, {"x": 5086.68, "y": 2421.11, "z": 62.43}, {"x": 5087.1, "y": 2420.93, "z": 62.46}, {"x": 5088.2, "y": 2420.16, "z": 62.58}], "right_lane_mark_type": "NONE", "successors": [38116066], "predecessors": [38116700], "right_neighbor_id": null, "left_neighbor_id": null}, "38116085": {"id": 38116085, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5264.72, "y": 2359.16, "z": 70.25}, {"x": 5245.56, "y": 2372.45, "z": 69.59}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5268.05, "y": 2363.91, "z": 70.19}, {"x": 5259.13, "y": 2369.98, "z": 69.9}, {"x": 5249.26, "y": 2376.85, "z": 69.59}, {"x": 5249.22, "y": 2376.88, "z": 69.58}], "right_lane_mark_type": "NONE", "successors": [38114340, 38114376], "predecessors": [38109382], "right_neighbor_id": null, "left_neighbor_id": null}, "38116233": {"id": 38116233, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5222.08, "y": 2453.05, "z": 68.15}, {"x": 5223.55, "y": 2452.05, "z": 68.17}, {"x": 5228.26, "y": 2448.83, "z": 68.32}, {"x": 5229.11, "y": 2448.25, "z": 68.37}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5220.21, "y": 2448.15, "z": 68.12}, {"x": 5221.88, "y": 2446.94, "z": 68.17}, {"x": 5226.28, "y": 2443.74, "z": 68.37}], "right_lane_mark_type": "NONE", "successors": [38116264], "predecessors": [38114698], "right_neighbor_id": null, "left_neighbor_id": null}, "38116264": {"id": 38116264, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5229.11, "y": 2448.25, "z": 68.37}, {"x": 5259.1, "y": 2427.54, "z": 69.6}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5226.28, "y": 2443.74, "z": 68.37}, {"x": 5256.18, "y": 2423.59, "z": 69.51}], "right_lane_mark_type": "NONE", "successors": [38114664, 38114673], "predecessors": [38116233, 38116298], "right_neighbor_id": null, "left_neighbor_id": null}, "38116297": {"id": 38116297, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5225.26, "y": 2453.64, "z": 68.28}, {"x": 5228.43, "y": 2458.41, "z": 68.39}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5229.14, "y": 2450.93, "z": 68.32}, {"x": 5232.62, "y": 2455.82, "z": 68.4}], "right_lane_mark_type": "NONE", "successors": [], "predecessors": [38116384], "right_neighbor_id": null, "left_neighbor_id": null}, "38116298": {"id": 38116298, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5229.15, "y": 2450.95, "z": 68.32}, {"x": 5228.19, "y": 2449.59, "z": 68.32}, {"x": 5228.15, "y": 2449.46, "z": 68.32}, {"x": 5228.05, "y": 2449.31, "z": 68.32}, {"x": 5228.1, "y": 2449.17, "z": 68.32}, {"x": 5228.11, "y": 2449.03, "z": 68.32}, {"x": 5228.19, "y": 2448.92, "z": 68.32}, {"x": 5228.24, "y": 2448.79, "z": 68.32}, {"x": 5229.11, "y": 2448.25, "z": 68.37}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5225.28, "y": 2453.66, "z": 68.28}, {"x": 5224.37, "y": 2452.3, "z": 68.21}, {"x": 5224.07, "y": 2452.09, "z": 68.19}, {"x": 5223.79, "y": 2451.67, "z": 68.18}, {"x": 5223.28, "y": 2450.4, "z": 68.16}, {"x": 5223.04, "y": 2448.86, "z": 68.16}, {"x": 5223.31, "y": 2447.15, "z": 68.2}, {"x": 5224.32, "y": 2445.41, "z": 68.28}, {"x": 5226.28, "y": 2443.74, "z": 68.37}], "right_lane_mark_type": "NONE", "successors": [38116264], "predecessors": [38116484], "right_neighbor_id": null, "left_neighbor_id": null}, "38116337": {"id": 38116337, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5256.07, "y": 2312.86, "z": 71.56}, {"x": 5256.73, "y": 2312.22, "z": 71.58}, {"x": 5258.42, "y": 2309.94, "z": 71.57}, {"x": 5258.92, "y": 2308.63, "z": 71.58}, {"x": 5259.06, "y": 2307.2, "z": 71.61}, {"x": 5258.72, "y": 2305.64, "z": 71.63}, {"x": 5257.8, "y": 2303.94, "z": 71.65}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5254.02, "y": 2309.45, "z": 71.64}, {"x": 5254.46, "y": 2308.77, "z": 71.63}, {"x": 5254.89, "y": 2307.87, "z": 71.61}, {"x": 5254.87, "y": 2307.16, "z": 71.61}, {"x": 5254.42, "y": 2306.47, "z": 71.63}], "right_lane_mark_type": "NONE", "successors": [38116470], "predecessors": [38115671], "right_neighbor_id": null, "left_neighbor_id": null}, "38116338": {"id": 38116338, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5256.07, "y": 2312.86, "z": 71.56}, {"x": 5261.92, "y": 2307.74, "z": 71.66}, {"x": 5261.92, "y": 2307.71, "z": 71.66}, {"x": 5262.4, "y": 2307.24, "z": 71.72}, {"x": 5262.87, "y": 2306.91, "z": 71.77}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5254.02, "y": 2309.45, "z": 71.64}, {"x": 5260.31, "y": 2305.39, "z": 71.7}, {"x": 5260.63, "y": 2305.2, "z": 71.72}, {"x": 5261.25, "y": 2304.78, "z": 71.77}], "right_lane_mark_type": "NONE", "successors": [38116016], "predecessors": [38115671], "right_neighbor_id": null, "left_neighbor_id": null}, "38116340": {"id": 38116340, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5250.0, "y": 2294.7, "z": 71.75}, {"x": 5242.61, "y": 2282.67, "z": 71.96}, {"x": 5240.13, "y": 2278.14, "z": 72.02}, {"x": 5234.68, "y": 2268.27, "z": 72.13}, {"x": 5226.43, "y": 2250.0, "z": 72.42}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5250.0, "y": 2299.5, "z": 71.78}, {"x": 5221.73, "y": 2252.38, "z": 72.43}, {"x": 5220.31, "y": 2250.0, "z": 72.46}], "right_lane_mark_type": "NONE", "successors": [38117885], "predecessors": [38116470], "right_neighbor_id": null, "left_neighbor_id": null}, "38116375": {"id": 38116375, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5262.32, "y": 2309.59, "z": 71.59}, {"x": 5258.14, "y": 2304.35, "z": 71.64}, {"x": 5257.8, "y": 2303.94, "z": 71.65}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5258.49, "y": 2312.06, "z": 71.56}, {"x": 5258.39, "y": 2311.9, "z": 71.56}, {"x": 5254.87, "y": 2307.16, "z": 71.61}, {"x": 5254.42, "y": 2306.47, "z": 71.63}], "right_lane_mark_type": "NONE", "successors": [38116470], "predecessors": [38115599], "right_neighbor_id": null, "left_neighbor_id": null}, "38116378": {"id": 38116378, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5294.97, "y": 2281.98, "z": 72.82}, {"x": 5261.25, "y": 2304.78, "z": 71.77}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5296.48, "y": 2284.83, "z": 72.77}, {"x": 5262.87, "y": 2306.91, "z": 71.77}], "right_lane_mark_type": "NONE", "successors": [38116651, 38116650], "predecessors": [38114310], "right_neighbor_id": null, "left_neighbor_id": null}, "38116384": {"id": 38116384, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5222.08, "y": 2453.05, "z": 68.15}, {"x": 5223.55, "y": 2452.05, "z": 68.17}, {"x": 5223.78, "y": 2452.02, "z": 68.19}, {"x": 5224.07, "y": 2452.09, "z": 68.19}, {"x": 5224.37, "y": 2452.3, "z": 68.21}, {"x": 5225.26, "y": 2453.64, "z": 68.28}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5220.21, "y": 2448.15, "z": 68.12}, {"x": 5220.68, "y": 2447.92, "z": 68.13}, {"x": 5221.75, "y": 2447.5, "z": 68.16}, {"x": 5223.26, "y": 2447.22, "z": 68.19}, {"x": 5225.03, "y": 2447.38, "z": 68.25}, {"x": 5226.9, "y": 2448.29, "z": 68.29}, {"x": 5228.61, "y": 2450.19, "z": 68.32}, {"x": 5229.14, "y": 2450.93, "z": 68.32}], "right_lane_mark_type": "NONE", "successors": [38116297], "predecessors": [38114698], "right_neighbor_id": null, "left_neighbor_id": null}, "38116425": {"id": 38116425, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5302.76, "y": 2398.06, "z": 70.61}, {"x": 5313.26, "y": 2391.01, "z": 71.07}, {"x": 5313.95, "y": 2390.16, "z": 71.06}, {"x": 5314.42, "y": 2389.98, "z": 71.08}, {"x": 5315.03, "y": 2389.93, "z": 71.11}, {"x": 5337.84, "y": 2373.86, "z": 71.66}, {"x": 5338.4, "y": 2373.55, "z": 71.66}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5299.9, "y": 2393.9, "z": 70.58}, {"x": 5335.67, "y": 2368.8, "z": 71.67}, {"x": 5335.75, "y": 2368.73, "z": 71.67}, {"x": 5335.93, "y": 2368.61, "z": 71.67}], "right_lane_mark_type": "NONE", "successors": [38109421, 38109292], "predecessors": [38115184, 38116557], "right_neighbor_id": null, "left_neighbor_id": null}, "38116470": {"id": 38116470, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5257.8, "y": 2303.94, "z": 71.65}, {"x": 5250.0, "y": 2294.7, "z": 71.75}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5254.42, "y": 2306.47, "z": 71.63}, {"x": 5250.0, "y": 2299.5, "z": 71.78}], "right_lane_mark_type": "NONE", "successors": [38116340], "predecessors": [38116337, 38116650, 38116375], "right_neighbor_id": null, "left_neighbor_id": null}, "38116473": {"id": 38116473, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5316.7, "y": 2419.79, "z": 70.48}, {"x": 5302.93, "y": 2400.3, "z": 70.56}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5311.88, "y": 2419.73, "z": 70.46}, {"x": 5299.36, "y": 2402.61, "z": 70.52}], "right_lane_mark_type": "NONE", "successors": [38115184], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "38116484": {"id": 38116484, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5232.62, "y": 2455.82, "z": 68.4}, {"x": 5229.15, "y": 2450.95, "z": 68.32}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5228.43, "y": 2458.41, "z": 68.39}, {"x": 5225.28, "y": 2453.66, "z": 68.28}], "right_lane_mark_type": "NONE", "successors": [38116298], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "38116487": {"id": 38116487, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5296.66, "y": 2402.37, "z": 70.48}, {"x": 5296.78, "y": 2402.29, "z": 70.49}, {"x": 5297.38, "y": 2401.89, "z": 70.53}, {"x": 5297.66, "y": 2401.71, "z": 70.53}, {"x": 5297.98, "y": 2401.68, "z": 70.55}, {"x": 5298.33, "y": 2401.69, "z": 70.53}, {"x": 5298.65, "y": 2401.74, "z": 70.51}, {"x": 5299.22, "y": 2402.42, "z": 70.52}, {"x": 5299.36, "y": 2402.61, "z": 70.52}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5293.79, "y": 2398.19, "z": 70.4}, {"x": 5294.68, "y": 2397.65, "z": 70.43}, {"x": 5295.89, "y": 2397.35, "z": 70.44}, {"x": 5297.28, "y": 2397.28, "z": 70.47}, {"x": 5298.77, "y": 2397.48, "z": 70.5}, {"x": 5300.26, "y": 2398.03, "z": 70.53}, {"x": 5301.69, "y": 2398.96, "z": 70.56}, {"x": 5302.96, "y": 2400.33, "z": 70.55}], "right_lane_mark_type": "NONE", "successors": [38116606], "predecessors": [38109262], "right_neighbor_id": null, "left_neighbor_id": null}, "38116534": {"id": 38116534, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5089.09, "y": 2421.29, "z": 62.55}, {"x": 5087.99, "y": 2422.05, "z": 62.48}, {"x": 5086.13, "y": 2423.01, "z": 62.38}, {"x": 5084.72, "y": 2423.19, "z": 62.32}, {"x": 5083.64, "y": 2422.8, "z": 62.28}, {"x": 5082.79, "y": 2422.03, "z": 62.26}, {"x": 5082.04, "y": 2421.08, "z": 62.25}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5090.79, "y": 2423.51, "z": 62.58}, {"x": 5089.6, "y": 2424.32, "z": 62.52}, {"x": 5087.83, "y": 2425.34, "z": 62.47}, {"x": 5086.18, "y": 2425.91, "z": 62.35}, {"x": 5084.7, "y": 2426.03, "z": 62.29}, {"x": 5083.37, "y": 2425.77, "z": 62.24}, {"x": 5082.19, "y": 2425.18, "z": 62.21}, {"x": 5081.13, "y": 2424.32, "z": 62.18}, {"x": 5080.16, "y": 2423.25, "z": 62.14}, {"x": 5079.28, "y": 2422.02, "z": 62.09}], "right_lane_mark_type": "NONE", "successors": [38116936], "predecessors": [38116727], "right_neighbor_id": null, "left_neighbor_id": null}, "38116557": {"id": 38116557, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5296.66, "y": 2402.37, "z": 70.48}, {"x": 5302.19, "y": 2398.47, "z": 70.58}, {"x": 5302.24, "y": 2398.41, "z": 70.58}, {"x": 5302.76, "y": 2398.06, "z": 70.61}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5293.79, "y": 2398.19, "z": 70.4}, {"x": 5299.9, "y": 2393.9, "z": 70.58}], "right_lane_mark_type": "NONE", "successors": [38116425], "predecessors": [38109262], "right_neighbor_id": null, "left_neighbor_id": null}, "38116606": {"id": 38116606, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5299.36, "y": 2402.61, "z": 70.52}, {"x": 5311.88, "y": 2419.73, "z": 70.46}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5302.96, "y": 2400.33, "z": 70.55}, {"x": 5316.7, "y": 2419.79, "z": 70.48}], "right_lane_mark_type": "NONE", "successors": [], "predecessors": [38116487], "right_neighbor_id": null, "left_neighbor_id": null}, "38116650": {"id": 38116650, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5261.25, "y": 2304.78, "z": 71.77}, {"x": 5260.36, "y": 2305.49, "z": 71.7}, {"x": 5259.68, "y": 2305.73, "z": 71.66}, {"x": 5258.92, "y": 2305.29, "z": 71.64}, {"x": 5257.91, "y": 2304.07, "z": 71.64}, {"x": 5257.8, "y": 2303.94, "z": 71.65}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5262.87, "y": 2306.91, "z": 71.77}, {"x": 5259.96, "y": 2308.24, "z": 71.6}, {"x": 5258.52, "y": 2308.39, "z": 71.59}, {"x": 5257.11, "y": 2308.21, "z": 71.59}, {"x": 5255.74, "y": 2307.57, "z": 71.6}, {"x": 5254.42, "y": 2306.47, "z": 71.63}], "right_lane_mark_type": "NONE", "successors": [38116470], "predecessors": [38116378], "right_neighbor_id": null, "left_neighbor_id": null}, "38116651": {"id": 38116651, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5261.25, "y": 2304.78, "z": 71.77}, {"x": 5260.63, "y": 2305.2, "z": 71.72}, {"x": 5260.34, "y": 2305.37, "z": 71.7}, {"x": 5254.01, "y": 2309.47, "z": 71.63}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5262.87, "y": 2306.91, "z": 71.77}, {"x": 5262.4, "y": 2307.24, "z": 71.72}, {"x": 5261.92, "y": 2307.71, "z": 71.66}, {"x": 5261.92, "y": 2307.74, "z": 71.66}, {"x": 5256.05, "y": 2312.88, "z": 71.56}], "right_lane_mark_type": "NONE", "successors": [38115208], "predecessors": [38116378], "right_neighbor_id": null, "left_neighbor_id": null}, "38116700": {"id": 38116700, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5045.15, "y": 2364.79, "z": 60.56}, {"x": 5082.04, "y": 2421.08, "z": 62.25}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5047.63, "y": 2363.08, "z": 60.59}, {"x": 5051.0, "y": 2368.54, "z": 60.62}, {"x": 5055.49, "y": 2375.57, "z": 60.85}, {"x": 5056.23, "y": 2376.61, "z": 60.91}, {"x": 5058.09, "y": 2379.47, "z": 61.07}, {"x": 5058.92, "y": 2380.54, "z": 61.13}, {"x": 5061.01, "y": 2383.65, "z": 61.26}, {"x": 5074.62, "y": 2404.43, "z": 62.03}, {"x": 5084.69, "y": 2420.0, "z": 62.3}], "right_lane_mark_type": "NONE", "successors": [38116069], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 38116936}, "38116727": {"id": 38116727, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5120.0, "y": 2400.0, "z": 64.35}, {"x": 5089.09, "y": 2421.29, "z": 62.55}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5125.13, "y": 2400.0, "z": 64.58}, {"x": 5090.79, "y": 2423.51, "z": 62.58}], "right_lane_mark_type": "NONE", "successors": [38116534], "predecessors": [38119598], "right_neighbor_id": null, "left_neighbor_id": null}, "38116936": {"id": 38116936, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5082.04, "y": 2421.08, "z": 62.25}, {"x": 5045.15, "y": 2364.79, "z": 60.56}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5079.28, "y": 2422.02, "z": 62.09}, {"x": 5064.28, "y": 2398.88, "z": 61.68}, {"x": 5057.12, "y": 2388.01, "z": 61.24}, {"x": 5055.79, "y": 2386.62, "z": 61.16}, {"x": 5055.18, "y": 2385.75, "z": 61.11}, {"x": 5045.34, "y": 2370.96, "z": 60.58}, {"x": 5042.53, "y": 2366.6, "z": 60.48}], "right_lane_mark_type": "NONE", "successors": [], "predecessors": [38116534], "right_neighbor_id": null, "left_neighbor_id": 38116700}, "38117100": {"id": 38117100, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5264.72, "y": 2359.16, "z": 70.25}, {"x": 5272.94, "y": 2353.69, "z": 70.51}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5260.02, "y": 2352.14, "z": 70.15}, {"x": 5268.73, "y": 2346.16, "z": 70.46}], "right_lane_mark_type": "NONE", "successors": [38109440, 38111858, 38109167], "predecessors": [38109359], "right_neighbor_id": null, "left_neighbor_id": 38109382}, "38117202": {"id": 38117202, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5220.63, "y": 2516.66, "z": 67.47}, {"x": 5213.07, "y": 2506.15, "z": 67.19}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5214.32, "y": 2521.21, "z": 67.33}, {"x": 5206.75, "y": 2510.71, "z": 67.09}], "right_lane_mark_type": "NONE", "successors": [38117242], "predecessors": [38117142], "right_neighbor_id": null, "left_neighbor_id": null}, "38117242": {"id": 38117242, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5213.07, "y": 2506.15, "z": 67.19}, {"x": 5201.69, "y": 2490.0, "z": 66.87}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5206.75, "y": 2510.71, "z": 67.09}, {"x": 5191.84, "y": 2490.0, "z": 66.62}], "right_lane_mark_type": "NONE", "successors": [38111376], "predecessors": [38117202, 38117303], "right_neighbor_id": null, "left_neighbor_id": null}, "38117303": {"id": 38117303, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5208.5, "y": 2516.78, "z": 67.22}, {"x": 5209.49, "y": 2515.8, "z": 67.24}, {"x": 5210.56, "y": 2514.69, "z": 67.27}, {"x": 5211.55, "y": 2513.4, "z": 67.31}, {"x": 5212.42, "y": 2511.99, "z": 67.33}, {"x": 5213.07, "y": 2510.51, "z": 67.29}, {"x": 5213.45, "y": 2509.01, "z": 67.24}, {"x": 5213.47, "y": 2507.54, "z": 67.22}, {"x": 5213.07, "y": 2506.15, "z": 67.19}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5206.09, "y": 2513.97, "z": 67.11}, {"x": 5207.08, "y": 2512.98, "z": 67.11}, {"x": 5207.58, "y": 2512.28, "z": 67.13}, {"x": 5207.38, "y": 2511.52, "z": 67.11}, {"x": 5206.75, "y": 2510.71, "z": 67.09}], "right_lane_mark_type": "NONE", "successors": [38117242], "predecessors": [38117102], "right_neighbor_id": null, "left_neighbor_id": null}, "38118150": {"id": 38118150, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5121.31, "y": 2370.0, "z": 64.41}, {"x": 5111.53, "y": 2354.08, "z": 64.06}, {"x": 5094.14, "y": 2325.55, "z": 63.5}, {"x": 5086.44, "y": 2312.85, "z": 63.25}, {"x": 5077.66, "y": 2298.28, "z": 62.96}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5111.89, "y": 2370.0, "z": 64.14}, {"x": 5070.82, "y": 2302.45, "z": 62.85}], "right_lane_mark_type": "NONE", "successors": [38118245, 38117835], "predecessors": [38119412], "right_neighbor_id": null, "left_neighbor_id": null}, "38118957": {"id": 38118957, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5137.17, "y": 2396.17, "z": 65.11}, {"x": 5132.79, "y": 2388.94, "z": 64.96}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5130.4, "y": 2400.29, "z": 64.88}, {"x": 5125.94, "y": 2392.89, "z": 64.68}, {"x": 5125.81, "y": 2392.68, "z": 64.69}], "right_lane_mark_type": "NONE", "successors": [38119753, 38119931], "predecessors": [38111342], "right_neighbor_id": null, "left_neighbor_id": null}, "38119080": {"id": 38119080, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5125.79, "y": 2397.91, "z": 64.63}, {"x": 5129.19, "y": 2396.33, "z": 64.79}, {"x": 5131.45, "y": 2394.78, "z": 64.91}, {"x": 5132.78, "y": 2393.31, "z": 64.95}, {"x": 5133.38, "y": 2391.97, "z": 64.95}, {"x": 5133.48, "y": 2390.81, "z": 64.94}, {"x": 5133.27, "y": 2389.89, "z": 64.93}, {"x": 5132.97, "y": 2389.25, "z": 64.96}, {"x": 5132.79, "y": 2388.94, "z": 64.96}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5124.28, "y": 2395.59, "z": 64.66}, {"x": 5125.22, "y": 2395.0, "z": 64.67}, {"x": 5125.33, "y": 2394.87, "z": 64.68}, {"x": 5125.67, "y": 2394.65, "z": 64.68}, {"x": 5125.88, "y": 2394.43, "z": 64.68}, {"x": 5126.01, "y": 2394.18, "z": 64.67}, {"x": 5126.03, "y": 2394.11, "z": 64.67}, {"x": 5126.04, "y": 2394.1, "z": 64.67}, {"x": 5126.04, "y": 2394.07, "z": 64.67}, {"x": 5126.07, "y": 2393.94, "z": 64.67}, {"x": 5126.09, "y": 2393.71, "z": 64.68}, {"x": 5126.06, "y": 2393.27, "z": 64.68}, {"x": 5125.93, "y": 2392.88, "z": 64.68}, {"x": 5125.81, "y": 2392.68, "z": 64.69}], "right_lane_mark_type": "NONE", "successors": [38119753, 38119931], "predecessors": [38119874], "right_neighbor_id": null, "left_neighbor_id": null}, "38119412": {"id": 38119412, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5127.59, "y": 2380.33, "z": 64.71}, {"x": 5121.31, "y": 2370.0, "z": 64.41}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5120.8, "y": 2384.53, "z": 64.52}, {"x": 5111.89, "y": 2370.0, "z": 64.14}], "right_lane_mark_type": "NONE", "successors": [38118150], "predecessors": [38119960, 38119753], "right_neighbor_id": null, "left_neighbor_id": null}, "38119413": {"id": 38119413, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5193.29, "y": 2472.78, "z": 66.61}, {"x": 5193.35, "y": 2472.73, "z": 66.62}, {"x": 5220.0, "y": 2454.47, "z": 67.99}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5189.42, "y": 2468.25, "z": 66.55}, {"x": 5193.11, "y": 2465.75, "z": 66.8}, {"x": 5194.56, "y": 2465.23, "z": 66.83}, {"x": 5195.83, "y": 2464.77, "z": 66.86}, {"x": 5220.0, "y": 2448.31, "z": 68.12}], "right_lane_mark_type": "NONE", "successors": [38114698], "predecessors": [38119952], "right_neighbor_id": null, "left_neighbor_id": null}, "38119598": {"id": 38119598, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5125.02, "y": 2396.73, "z": 64.63}, {"x": 5120.0, "y": 2400.0, "z": 64.35}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5126.51, "y": 2399.05, "z": 64.68}, {"x": 5126.28, "y": 2399.21, "z": 64.66}, {"x": 5126.19, "y": 2399.27, "z": 64.66}, {"x": 5125.13, "y": 2400.0, "z": 64.58}], "right_lane_mark_type": "NONE", "successors": [38116727], "predecessors": [38119599], "right_neighbor_id": null, "left_neighbor_id": null}, "38119599": {"id": 38119599, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5137.17, "y": 2396.17, "z": 65.11}, {"x": 5135.95, "y": 2394.66, "z": 65.05}, {"x": 5134.48, "y": 2393.81, "z": 64.99}, {"x": 5132.85, "y": 2393.51, "z": 64.96}, {"x": 5131.13, "y": 2393.67, "z": 64.9}, {"x": 5129.4, "y": 2394.17, "z": 64.82}, {"x": 5127.76, "y": 2394.92, "z": 64.73}, {"x": 5126.27, "y": 2395.8, "z": 64.69}, {"x": 5125.02, "y": 2396.73, "z": 64.63}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5130.4, "y": 2400.29, "z": 64.88}, {"x": 5130.01, "y": 2399.8, "z": 64.84}, {"x": 5129.91, "y": 2399.71, "z": 64.84}, {"x": 5129.76, "y": 2399.54, "z": 64.83}, {"x": 5129.29, "y": 2399.14, "z": 64.79}, {"x": 5128.85, "y": 2398.82, "z": 64.77}, {"x": 5128.25, "y": 2398.63, "z": 64.74}, {"x": 5127.99, "y": 2398.62, "z": 64.74}, {"x": 5127.74, "y": 2398.67, "z": 64.73}, {"x": 5127.16, "y": 2398.75, "z": 64.72}, {"x": 5126.69, "y": 2398.93, "z": 64.71}, {"x": 5126.51, "y": 2399.05, "z": 64.68}], "right_lane_mark_type": "NONE", "successors": [38119598], "predecessors": [38111342], "right_neighbor_id": null, "left_neighbor_id": null}, "38119753": {"id": 38119753, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5132.79, "y": 2388.94, "z": 64.96}, {"x": 5127.59, "y": 2380.33, "z": 64.71}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5125.81, "y": 2392.68, "z": 64.69}, {"x": 5120.8, "y": 2384.53, "z": 64.52}], "right_lane_mark_type": "NONE", "successors": [38119412], "predecessors": [38119080, 38118957], "right_neighbor_id": null, "left_neighbor_id": null}, "38119868": {"id": 38119868, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5133.2, "y": 2385.48, "z": 64.88}, {"x": 5133.24, "y": 2385.46, "z": 64.88}, {"x": 5133.26, "y": 2385.44, "z": 64.88}, {"x": 5154.86, "y": 2370.0, "z": 66.34}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5130.64, "y": 2382.18, "z": 64.81}, {"x": 5131.19, "y": 2381.85, "z": 64.82}, {"x": 5131.38, "y": 2381.7, "z": 64.85}, {"x": 5132.32, "y": 2381.06, "z": 64.91}, {"x": 5132.78, "y": 2380.51, "z": 64.97}, {"x": 5135.8, "y": 2377.72, "z": 65.2}, {"x": 5148.63, "y": 2370.0, "z": 66.08}], "right_lane_mark_type": "NONE", "successors": [38114336], "predecessors": [38119931], "right_neighbor_id": null, "left_neighbor_id": null}, "38119874": {"id": 38119874, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5122.49, "y": 2400.0, "z": 64.42}, {"x": 5125.79, "y": 2397.91, "z": 64.63}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5117.72, "y": 2400.0, "z": 64.3}, {"x": 5121.38, "y": 2397.54, "z": 64.51}, {"x": 5123.18, "y": 2396.32, "z": 64.61}, {"x": 5123.4, "y": 2396.17, "z": 64.62}, {"x": 5124.28, "y": 2395.59, "z": 64.66}], "right_lane_mark_type": "NONE", "successors": [38119080], "predecessors": [38116066], "right_neighbor_id": null, "left_neighbor_id": null}, "38119931": {"id": 38119931, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5132.79, "y": 2388.94, "z": 64.96}, {"x": 5132.68, "y": 2388.55, "z": 64.94}, {"x": 5132.59, "y": 2388.35, "z": 64.93}, {"x": 5132.4, "y": 2387.73, "z": 64.89}, {"x": 5132.37, "y": 2387.41, "z": 64.88}, {"x": 5132.35, "y": 2387.33, "z": 64.88}, {"x": 5132.36, "y": 2387.24, "z": 64.88}, {"x": 5132.35, "y": 2386.79, "z": 64.86}, {"x": 5132.51, "y": 2386.28, "z": 64.86}, {"x": 5132.61, "y": 2386.14, "z": 64.86}, {"x": 5132.81, "y": 2385.75, "z": 64.86}, {"x": 5133.2, "y": 2385.48, "z": 64.88}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5125.81, "y": 2392.68, "z": 64.69}, {"x": 5125.57, "y": 2390.93, "z": 64.68}, {"x": 5125.59, "y": 2389.0, "z": 64.69}, {"x": 5126.01, "y": 2387.27, "z": 64.72}, {"x": 5126.72, "y": 2385.77, "z": 64.75}, {"x": 5127.59, "y": 2384.52, "z": 64.75}, {"x": 5128.52, "y": 2383.56, "z": 64.75}, {"x": 5130.11, "y": 2382.57, "z": 64.8}, {"x": 5130.64, "y": 2382.18, "z": 64.81}], "right_lane_mark_type": "NONE", "successors": [38119868], "predecessors": [38119080, 38118957], "right_neighbor_id": null, "left_neighbor_id": null}, "38119950": {"id": 38119950, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5151.93, "y": 2370.0, "z": 66.2}, {"x": 5131.05, "y": 2382.68, "z": 64.8}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5160.02, "y": 2370.0, "z": 66.66}, {"x": 5150.18, "y": 2376.35, "z": 66.08}, {"x": 5148.08, "y": 2377.2, "z": 65.95}, {"x": 5143.52, "y": 2379.06, "z": 65.53}, {"x": 5133.78, "y": 2385.1, "z": 64.92}, {"x": 5133.18, "y": 2385.48, "z": 64.87}], "right_lane_mark_type": "NONE", "successors": [38119960], "predecessors": [38120641], "right_neighbor_id": null, "left_neighbor_id": null}, "38119951": {"id": 38119951, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5192.71, "y": 2477.54, "z": 66.59}, {"x": 5191.41, "y": 2475.75, "z": 66.57}, {"x": 5191.22, "y": 2475.4, "z": 66.56}, {"x": 5186.94, "y": 2469.42, "z": 66.48}, {"x": 5186.86, "y": 2469.41, "z": 66.47}, {"x": 5186.63, "y": 2469.25, "z": 66.47}, {"x": 5185.06, "y": 2467.04, "z": 66.43}, {"x": 5183.61, "y": 2464.83, "z": 66.42}, {"x": 5169.09, "y": 2444.5, "z": 65.98}, {"x": 5166.37, "y": 2440.79, "z": 65.95}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5186.24, "y": 2482.2, "z": 66.46}, {"x": 5176.6, "y": 2468.77, "z": 66.23}, {"x": 5172.99, "y": 2463.63, "z": 66.18}, {"x": 5166.14, "y": 2454.19, "z": 65.95}, {"x": 5160.14, "y": 2445.81, "z": 65.74}], "right_lane_mark_type": "NONE", "successors": [38111879, 38111884, 38111310, 38111873], "predecessors": [38111376], "right_neighbor_id": null, "left_neighbor_id": null}, "38119952": {"id": 38119952, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5192.71, "y": 2477.54, "z": 66.59}, {"x": 5192.04, "y": 2476.75, "z": 66.57}, {"x": 5191.46, "y": 2475.96, "z": 66.57}, {"x": 5191.42, "y": 2475.77, "z": 66.57}, {"x": 5191.41, "y": 2475.75, "z": 66.57}, {"x": 5191.17, "y": 2475.29, "z": 66.55}, {"x": 5191.11, "y": 2474.94, "z": 66.54}, {"x": 5191.22, "y": 2474.57, "z": 66.55}, {"x": 5191.51, "y": 2474.24, "z": 66.57}, {"x": 5192.45, "y": 2473.5, "z": 66.59}, {"x": 5193.29, "y": 2472.78, "z": 66.61}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5186.24, "y": 2482.2, "z": 66.46}, {"x": 5185.14, "y": 2480.79, "z": 66.43}, {"x": 5184.44, "y": 2479.28, "z": 66.39}, {"x": 5184.1, "y": 2477.72, "z": 66.38}, {"x": 5184.1, "y": 2476.13, "z": 66.42}, {"x": 5184.4, "y": 2474.56, "z": 66.47}, {"x": 5184.98, "y": 2473.04, "z": 66.5}, {"x": 5185.79, "y": 2471.61, "z": 66.48}, {"x": 5186.83, "y": 2470.31, "z": 66.46}, {"x": 5188.04, "y": 2469.18, "z": 66.5}, {"x": 5188.65, "y": 2468.77, "z": 66.52}, {"x": 5188.68, "y": 2468.75, "z": 66.52}, {"x": 5189.42, "y": 2468.25, "z": 66.55}], "right_lane_mark_type": "NONE", "successors": [38119413], "predecessors": [38111376], "right_neighbor_id": null, "left_neighbor_id": null}, "38119960": {"id": 38119960, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5131.05, "y": 2382.68, "z": 64.8}, {"x": 5129.63, "y": 2382.82, "z": 64.78}, {"x": 5129.05, "y": 2382.5, "z": 64.78}, {"x": 5128.73, "y": 2382.21, "z": 64.77}, {"x": 5128.28, "y": 2381.47, "z": 64.74}, {"x": 5127.59, "y": 2380.33, "z": 64.71}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5133.18, "y": 2385.48, "z": 64.87}, {"x": 5131.28, "y": 2386.73, "z": 64.83}, {"x": 5129.54, "y": 2387.52, "z": 64.82}, {"x": 5127.95, "y": 2387.9, "z": 64.8}, {"x": 5126.5, "y": 2387.92, "z": 64.74}, {"x": 5125.17, "y": 2387.64, "z": 64.68}, {"x": 5123.95, "y": 2387.11, "z": 64.62}, {"x": 5122.83, "y": 2386.38, "z": 64.58}, {"x": 5121.78, "y": 2385.5, "z": 64.54}, {"x": 5120.8, "y": 2384.53, "z": 64.52}], "right_lane_mark_type": "NONE", "successors": [38119412], "predecessors": [38119950], "right_neighbor_id": null, "left_neighbor_id": null}, "38119984": {"id": 38119984, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5174.16, "y": 2356.73, "z": 67.63}, {"x": 5173.1, "y": 2356.86, "z": 67.57}, {"x": 5171.73, "y": 2356.5, "z": 67.5}, {"x": 5171.46, "y": 2356.26, "z": 67.49}, {"x": 5171.02, "y": 2356.14, "z": 67.47}, {"x": 5170.73, "y": 2355.66, "z": 67.47}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5175.92, "y": 2359.61, "z": 67.7}, {"x": 5174.59, "y": 2360.44, "z": 67.65}, {"x": 5173.11, "y": 2360.56, "z": 67.54}, {"x": 5171.5, "y": 2360.31, "z": 67.41}, {"x": 5169.97, "y": 2359.71, "z": 67.33}, {"x": 5168.64, "y": 2358.81, "z": 67.3}, {"x": 5167.63, "y": 2357.63, "z": 67.27}], "right_lane_mark_type": "NONE", "successors": [38120280], "predecessors": [38114335], "right_neighbor_id": null, "left_neighbor_id": null}, "38120026": {"id": 38120026, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5167.65, "y": 2357.66, "z": 67.27}, {"x": 5167.82, "y": 2357.96, "z": 67.29}, {"x": 5167.72, "y": 2358.69, "z": 67.27}, {"x": 5167.67, "y": 2358.72, "z": 67.26}, {"x": 5167.65, "y": 2359.86, "z": 67.23}, {"x": 5166.88, "y": 2361.07, "z": 67.17}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5170.71, "y": 2355.63, "z": 67.47}, {"x": 5171.38, "y": 2357.24, "z": 67.45}, {"x": 5171.6, "y": 2358.93, "z": 67.42}, {"x": 5171.42, "y": 2360.58, "z": 67.4}, {"x": 5170.88, "y": 2362.11, "z": 67.38}, {"x": 5170.03, "y": 2363.42, "z": 67.32}, {"x": 5168.92, "y": 2364.4, "z": 67.26}], "right_lane_mark_type": "NONE", "successors": [38120641], "predecessors": [38120362], "right_neighbor_id": null, "left_neighbor_id": null}, "38120064": {"id": 38120064, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5174.94, "y": 2357.99, "z": 67.64}, {"x": 5205.22, "y": 2339.87, "z": 69.63}, {"x": 5209.29, "y": 2337.42, "z": 69.87}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5173.36, "y": 2355.34, "z": 67.67}, {"x": 5207.16, "y": 2335.32, "z": 69.86}, {"x": 5207.69, "y": 2334.98, "z": 69.87}], "right_lane_mark_type": "NONE", "successors": [38114355, 38114407], "predecessors": [38120363, 38120167], "right_neighbor_id": null, "left_neighbor_id": null}, "38120167": {"id": 38120167, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5167.66, "y": 2362.35, "z": 67.19}, {"x": 5174.94, "y": 2357.99, "z": 67.64}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5166.06, "y": 2359.67, "z": 67.17}, {"x": 5173.36, "y": 2355.34, "z": 67.67}], "right_lane_mark_type": "NONE", "successors": [38120064], "predecessors": [38114336], "right_neighbor_id": null, "left_neighbor_id": null}, "38120280": {"id": 38120280, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5170.73, "y": 2355.66, "z": 67.47}, {"x": 5166.47, "y": 2348.63, "z": 67.35}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5167.63, "y": 2357.63, "z": 67.27}, {"x": 5163.46, "y": 2350.39, "z": 67.17}], "right_lane_mark_type": "NONE", "successors": [], "predecessors": [38119984, 38120430], "right_neighbor_id": null, "left_neighbor_id": null}, "38120362": {"id": 38120362, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5163.44, "y": 2350.35, "z": 67.17}, {"x": 5165.31, "y": 2353.6, "z": 67.18}, {"x": 5165.65, "y": 2354.2, "z": 67.19}, {"x": 5167.65, "y": 2357.66, "z": 67.27}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5166.49, "y": 2348.66, "z": 67.35}, {"x": 5169.74, "y": 2354.03, "z": 67.51}, {"x": 5169.89, "y": 2354.46, "z": 67.46}, {"x": 5170.71, "y": 2355.63, "z": 67.47}], "right_lane_mark_type": "NONE", "successors": [38120026, 38120363], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "38120363": {"id": 38120363, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5167.65, "y": 2357.66, "z": 67.27}, {"x": 5168.81, "y": 2358.7, "z": 67.31}, {"x": 5170.09, "y": 2359.26, "z": 67.35}, {"x": 5171.37, "y": 2359.43, "z": 67.4}, {"x": 5172.53, "y": 2359.27, "z": 67.48}, {"x": 5173.47, "y": 2358.87, "z": 67.55}, {"x": 5174.94, "y": 2357.99, "z": 67.64}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5170.71, "y": 2355.63, "z": 67.47}, {"x": 5171.02, "y": 2356.14, "z": 67.47}, {"x": 5171.69, "y": 2356.32, "z": 67.51}, {"x": 5171.88, "y": 2356.21, "z": 67.54}, {"x": 5173.36, "y": 2355.34, "z": 67.67}], "right_lane_mark_type": "NONE", "successors": [38120064], "predecessors": [38120362], "right_neighbor_id": null, "left_neighbor_id": null}, "38120430": {"id": 38120430, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5167.66, "y": 2362.35, "z": 67.19}, {"x": 5169.08, "y": 2361.5, "z": 67.27}, {"x": 5170.07, "y": 2360.68, "z": 67.33}, {"x": 5170.76, "y": 2359.53, "z": 67.37}, {"x": 5171.15, "y": 2356.87, "z": 67.44}, {"x": 5170.73, "y": 2355.66, "z": 67.47}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5166.06, "y": 2359.67, "z": 67.17}, {"x": 5167.72, "y": 2358.69, "z": 67.27}, {"x": 5167.82, "y": 2357.96, "z": 67.29}, {"x": 5167.63, "y": 2357.63, "z": 67.27}], "right_lane_mark_type": "NONE", "successors": [38120280], "predecessors": [38114336], "right_neighbor_id": null, "left_neighbor_id": null}, "38120641": {"id": 38120641, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.88, "y": 2361.07, "z": 67.17}, {"x": 5151.93, "y": 2370.0, "z": 66.2}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5168.92, "y": 2364.4, "z": 67.26}, {"x": 5160.02, "y": 2370.0, "z": 66.66}], "right_lane_mark_type": "NONE", "successors": [38119950], "predecessors": [38120769, 38120026], "right_neighbor_id": null, "left_neighbor_id": null}, "38120769": {"id": 38120769, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5174.16, "y": 2356.73, "z": 67.63}, {"x": 5166.88, "y": 2361.07, "z": 67.17}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5175.92, "y": 2359.61, "z": 67.7}, {"x": 5171.86, "y": 2362.15, "z": 67.45}, {"x": 5171.4, "y": 2362.4, "z": 67.42}, {"x": 5170.95, "y": 2362.59, "z": 67.38}, {"x": 5170.61, "y": 2362.78, "z": 67.36}, {"x": 5170.52, "y": 2363.31, "z": 67.35}, {"x": 5168.92, "y": 2364.4, "z": 67.26}], "right_lane_mark_type": "NONE", "successors": [38120641], "predecessors": [38114335], "right_neighbor_id": null, "left_neighbor_id": null}, "38133153": {"id": 38133153, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5178.75, "y": 2414.34, "z": 66.8}, {"x": 5203.94, "y": 2397.25, "z": 67.92}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5175.57, "y": 2409.78, "z": 66.79}, {"x": 5178.53, "y": 2407.76, "z": 66.9}, {"x": 5185.09, "y": 2403.28, "z": 67.18}, {"x": 5200.63, "y": 2392.74, "z": 67.86}], "right_lane_mark_type": "NONE", "successors": [38114433], "predecessors": [38133155], "right_neighbor_id": null, "left_neighbor_id": 38133156}, "38133154": {"id": 38133154, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5164.69, "y": 2425.75, "z": 66.29}, {"x": 5180.46, "y": 2416.73, "z": 66.82}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5160.51, "y": 2419.95, "z": 66.15}, {"x": 5178.75, "y": 2414.34, "z": 66.8}], "right_lane_mark_type": "NONE", "successors": [38133156], "predecessors": [38111243, 38111879], "right_neighbor_id": null, "left_neighbor_id": null}, "38133155": {"id": 38133155, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5164.69, "y": 2425.75, "z": 66.29}, {"x": 5178.75, "y": 2414.34, "z": 66.8}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5160.51, "y": 2419.95, "z": 66.15}, {"x": 5173.62, "y": 2411.12, "z": 66.71}, {"x": 5175.57, "y": 2409.78, "z": 66.79}], "right_lane_mark_type": "NONE", "successors": [38133153], "predecessors": [38111243, 38111879], "right_neighbor_id": null, "left_neighbor_id": null}, "38133156": {"id": 38133156, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5180.46, "y": 2416.73, "z": 66.82}, {"x": 5185.87, "y": 2413.3, "z": 67.05}, {"x": 5205.75, "y": 2399.69, "z": 67.93}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5178.75, "y": 2414.34, "z": 66.8}, {"x": 5203.94, "y": 2397.25, "z": 67.92}], "right_lane_mark_type": "NONE", "successors": [38114426], "predecessors": [38133154], "right_neighbor_id": 38133153, "left_neighbor_id": 38110982}}, "drivable_areas": {"1225617": {"area_boundary": [{"x": 5294.97, "y": 2281.98, "z": 72.82}, {"x": 5261.25, "y": 2304.78, "z": 71.77}, {"x": 5262.87, "y": 2306.91, "z": 71.77}, {"x": 5296.48, "y": 2284.83, "z": 72.77}], "id": 1225617}, "1225544": {"area_boundary": [{"x": 5250.0, "y": 2299.5, "z": 71.78}, {"x": 5254.87, "y": 2307.16, "z": 71.61}, {"x": 5254.89, "y": 2307.87, "z": 71.61}, {"x": 5254.53, "y": 2308.51, "z": 71.63}, {"x": 5253.92, "y": 2309.64, "z": 71.63}, {"x": 5252.61, "y": 2311.09, "z": 71.59}, {"x": 5251.54, "y": 2312.09, "z": 71.57}, {"x": 5240.51, "y": 2319.7, "z": 71.41}, {"x": 5224.76, "y": 2328.95, "z": 70.43}, {"x": 5226.56, "y": 2331.45, "z": 70.42}, {"x": 5256.86, "y": 2312.36, "z": 71.57}, {"x": 5257.62, "y": 2311.97, "z": 71.57}, {"x": 5257.95, "y": 2311.81, "z": 71.56}, {"x": 5258.39, "y": 2311.9, "z": 71.56}, {"x": 5258.6, "y": 2312.24, "z": 71.56}, {"x": 5258.45, "y": 2312.7, "z": 71.56}, {"x": 5264.99, "y": 2324.16, "z": 71.38}, {"x": 5265.5, "y": 2324.2, "z": 71.36}, {"x": 5266.02, "y": 2324.61, "z": 71.35}, {"x": 5266.24, "y": 2325.24, "z": 71.34}, {"x": 5266.13, "y": 2325.83, "z": 71.38}, {"x": 5269.43, "y": 2331.55, "z": 71.14}, {"x": 5274.25, "y": 2328.25, "z": 71.24}, {"x": 5268.13, "y": 2319.68, "z": 71.4}, {"x": 5262.15, "y": 2309.28, "z": 71.6}, {"x": 5261.95, "y": 2308.72, "z": 71.61}, {"x": 5261.92, "y": 2308.24, "z": 71.65}, {"x": 5261.92, "y": 2307.71, "z": 71.66}, {"x": 5262.4, "y": 2307.24, "z": 71.72}, {"x": 5262.87, "y": 2306.91, "z": 71.77}, {"x": 5261.25, "y": 2304.78, "z": 71.77}, {"x": 5260.63, "y": 2305.2, "z": 71.72}, {"x": 5259.98, "y": 2305.58, "z": 71.67}, {"x": 5259.51, "y": 2305.63, "z": 71.65}, {"x": 5259.04, "y": 2305.25, "z": 71.64}, {"x": 5258.3, "y": 2304.54, "z": 71.64}, {"x": 5250.0, "y": 2294.7, "z": 71.75}], "id": 1225544}, "1225511": {"area_boundary": [{"x": 5250.0, "y": 2299.5, "z": 71.78}, {"x": 5250.0, "y": 2294.7, "z": 71.75}, {"x": 5242.61, "y": 2282.67, "z": 71.96}, {"x": 5235.17, "y": 2269.07, "z": 72.11}, {"x": 5227.09, "y": 2250.0, "z": 72.42}, {"x": 5220.31, "y": 2250.0, "z": 72.46}, {"x": 5232.87, "y": 2271.08, "z": 72.19}, {"x": 5243.16, "y": 2288.44, "z": 71.97}, {"x": 5249.42, "y": 2299.27, "z": 71.8}], "id": 1225511}, "1224874": {"area_boundary": [{"x": 5080.18, "y": 2464.32, "z": 62.58}, {"x": 5078.09, "y": 2464.14, "z": 62.46}, {"x": 5075.34, "y": 2463.47, "z": 62.32}, {"x": 5072.91, "y": 2462.71, "z": 62.19}, {"x": 5070.14, "y": 2461.67, "z": 62.04}, {"x": 5066.28, "y": 2466.97, "z": 62.14}, {"x": 5068.46, "y": 2468.29, "z": 62.32}, {"x": 5070.52, "y": 2469.0, "z": 62.42}, {"x": 5072.35, "y": 2469.6, "z": 62.54}, {"x": 5074.12, "y": 2470.05, "z": 62.55}, {"x": 5076.67, "y": 2470.57, "z": 62.68}, {"x": 5078.37, "y": 2470.74, "z": 62.76}, {"x": 5086.92, "y": 2464.32, "z": 62.93}, {"x": 5084.06, "y": 2464.5, "z": 62.76}, {"x": 5082.12, "y": 2464.39, "z": 62.67}], "id": 1224874}, "1224872": {"area_boundary": [{"x": 5032.17, "y": 2470.56, "z": 60.83}, {"x": 5026.74, "y": 2468.65, "z": 60.64}, {"x": 5021.65, "y": 2466.92, "z": 60.43}, {"x": 5017.01, "y": 2465.33, "z": 60.29}, {"x": 5010.0, "y": 2463.0, "z": 60.01}, {"x": 5010.0, "y": 2471.68, "z": 60.33}, {"x": 5012.35, "y": 2472.57, "z": 60.4}, {"x": 5018.51, "y": 2474.9, "z": 60.63}, {"x": 5024.16, "y": 2477.05, "z": 60.81}, {"x": 5029.58, "y": 2479.22, "z": 60.97}, {"x": 5032.94, "y": 2480.43, "z": 61.08}, {"x": 5036.87, "y": 2482.05, "z": 61.22}, {"x": 5039.36, "y": 2482.91, "z": 61.31}, {"x": 5041.9, "y": 2483.91, "z": 61.41}, {"x": 5041.65, "y": 2484.42, "z": 61.4}, {"x": 5038.09, "y": 2483.1, "z": 61.26}, {"x": 5032.1, "y": 2480.81, "z": 61.05}, {"x": 5026.13, "y": 2478.59, "z": 60.88}, {"x": 5010.0, "y": 2472.58, "z": 60.31}, {"x": 5010.0, "y": 2479.16, "z": 60.3}, {"x": 5021.51, "y": 2483.6, "z": 60.75}, {"x": 5022.34, "y": 2484.0, "z": 60.76}, {"x": 5022.49, "y": 2484.35, "z": 60.77}, {"x": 5022.39, "y": 2484.82, "z": 60.81}, {"x": 5020.46, "y": 2490.53, "z": 61.09}, {"x": 5030.18, "y": 2494.2, "z": 61.41}, {"x": 5032.12, "y": 2488.32, "z": 61.08}, {"x": 5032.23, "y": 2487.98, "z": 61.06}, {"x": 5032.47, "y": 2487.82, "z": 61.06}, {"x": 5033.48, "y": 2488.16, "z": 61.1}, {"x": 5036.66, "y": 2489.39, "z": 61.23}, {"x": 5044.46, "y": 2492.36, "z": 61.48}, {"x": 5044.99, "y": 2492.6, "z": 61.5}, {"x": 5045.31, "y": 2492.97, "z": 61.5}, {"x": 5045.34, "y": 2493.39, "z": 61.52}, {"x": 5045.12, "y": 2493.95, "z": 61.55}, {"x": 5043.02, "y": 2498.31, "z": 61.72}, {"x": 5048.12, "y": 2500.2, "z": 61.93}, {"x": 5049.07, "y": 2498.09, "z": 61.82}, {"x": 5049.86, "y": 2496.78, "z": 61.79}, {"x": 5050.34, "y": 2496.29, "z": 61.76}, {"x": 5050.81, "y": 2496.19, "z": 61.75}, {"x": 5051.33, "y": 2496.3, "z": 61.77}, {"x": 5051.68, "y": 2496.6, "z": 61.8}, {"x": 5052.49, "y": 2497.28, "z": 61.86}, {"x": 5053.3, "y": 2498.09, "z": 61.93}, {"x": 5053.9, "y": 2498.77, "z": 61.95}, {"x": 5054.4, "y": 2499.48, "z": 62.0}, {"x": 5055.04, "y": 2500.51, "z": 62.02}, {"x": 5055.56, "y": 2501.66, "z": 62.07}, {"x": 5056.12, "y": 2503.21, "z": 62.14}, {"x": 5057.02, "y": 2505.22, "z": 62.22}, {"x": 5057.57, "y": 2506.99, "z": 62.35}, {"x": 5069.33, "y": 2504.24, "z": 62.53}, {"x": 5069.56, "y": 2501.28, "z": 62.48}, {"x": 5070.11, "y": 2501.02, "z": 62.48}, {"x": 5070.17, "y": 2502.82, "z": 62.52}, {"x": 5069.97, "y": 2505.98, "z": 62.6}, {"x": 5069.18, "y": 2509.56, "z": 62.7}, {"x": 5068.14, "y": 2512.67, "z": 62.78}, {"x": 5066.3, "y": 2516.09, "z": 62.85}, {"x": 5065.6, "y": 2517.23, "z": 62.88}, {"x": 5065.26, "y": 2516.98, "z": 62.87}, {"x": 5066.36, "y": 2514.95, "z": 62.83}, {"x": 5057.89, "y": 2512.36, "z": 62.64}, {"x": 5057.83, "y": 2513.45, "z": 62.69}, {"x": 5057.66, "y": 2514.59, "z": 62.76}, {"x": 5057.34, "y": 2516.12, "z": 62.82}, {"x": 5056.83, "y": 2517.76, "z": 62.85}, {"x": 5056.08, "y": 2519.65, "z": 62.91}, {"x": 5055.18, "y": 2521.41, "z": 62.97}, {"x": 5054.37, "y": 2522.67, "z": 63.0}, {"x": 5053.45, "y": 2523.77, "z": 63.04}, {"x": 5051.85, "y": 2525.53, "z": 63.07}, {"x": 5051.39, "y": 2525.57, "z": 63.08}, {"x": 5050.11, "y": 2525.49, "z": 63.07}, {"x": 5049.61, "y": 2525.39, "z": 63.08}, {"x": 5047.85, "y": 2524.72, "z": 63.17}, {"x": 5042.85, "y": 2522.79, "z": 63.22}, {"x": 5042.19, "y": 2522.69, "z": 63.2}, {"x": 5039.48, "y": 2522.45, "z": 63.14}, {"x": 5038.71, "y": 2522.26, "z": 63.11}, {"x": 5010.0, "y": 2510.75, "z": 62.5}, {"x": 5010.0, "y": 2515.69, "z": 62.6}, {"x": 5013.73, "y": 2517.3, "z": 62.69}, {"x": 5019.91, "y": 2519.88, "z": 62.83}, {"x": 5040.96, "y": 2527.69, "z": 63.31}, {"x": 5042.51, "y": 2528.46, "z": 63.34}, {"x": 5043.75, "y": 2529.25, "z": 63.34}, {"x": 5044.57, "y": 2529.95, "z": 63.32}, {"x": 5044.95, "y": 2530.37, "z": 63.31}, {"x": 5045.1, "y": 2530.88, "z": 63.31}, {"x": 5045.23, "y": 2531.45, "z": 63.33}, {"x": 5045.09, "y": 2531.94, "z": 63.35}, {"x": 5044.55, "y": 2532.63, "z": 63.39}, {"x": 5039.8, "y": 2537.2, "z": 63.57}, {"x": 5036.86, "y": 2539.98, "z": 63.66}, {"x": 5030.58, "y": 2545.93, "z": 63.93}, {"x": 5026.3, "y": 2550.0, "z": 64.1}, {"x": 5040.06, "y": 2550.0, "z": 63.92}, {"x": 5049.46, "y": 2540.93, "z": 63.59}, {"x": 5055.0, "y": 2535.78, "z": 63.43}, {"x": 5058.85, "y": 2532.21, "z": 63.25}, {"x": 5059.94, "y": 2531.29, "z": 63.16}, {"x": 5060.53, "y": 2531.14, "z": 63.15}, {"x": 5061.19, "y": 2530.96, "z": 63.12}, {"x": 5062.06, "y": 2530.98, "z": 63.03}, {"x": 5062.61, "y": 2531.04, "z": 62.96}, {"x": 5063.13, "y": 2531.39, "z": 62.95}, {"x": 5064.46, "y": 2532.57, "z": 62.99}, {"x": 5068.87, "y": 2537.16, "z": 63.18}, {"x": 5069.88, "y": 2538.22, "z": 63.24}, {"x": 5071.15, "y": 2539.45, "z": 63.34}, {"x": 5072.36, "y": 2540.65, "z": 63.41}, {"x": 5074.35, "y": 2542.69, "z": 63.52}, {"x": 5076.31, "y": 2544.76, "z": 63.57}, {"x": 5076.37, "y": 2545.29, "z": 63.59}, {"x": 5075.81, "y": 2545.84, "z": 63.61}, {"x": 5074.04, "y": 2547.26, "z": 63.66}, {"x": 5070.49, "y": 2550.15, "z": 63.79}, {"x": 5070.5, "y": 2560.51, "z": 64.08}, {"x": 5079.14, "y": 2553.89, "z": 63.95}, {"x": 5082.58, "y": 2551.11, "z": 63.82}, {"x": 5083.24, "y": 2551.01, "z": 63.84}, {"x": 5083.64, "y": 2551.15, "z": 63.85}, {"x": 5086.13, "y": 2553.56, "z": 63.95}, {"x": 5086.13, "y": 2553.79, "z": 63.96}, {"x": 5085.96, "y": 2554.08, "z": 63.96}, {"x": 5083.39, "y": 2556.21, "z": 64.19}, {"x": 5088.44, "y": 2560.34, "z": 64.31}, {"x": 5089.85, "y": 2558.34, "z": 64.13}, {"x": 5090.17, "y": 2558.01, "z": 64.1}, {"x": 5090.38, "y": 2557.99, "z": 64.1}, {"x": 5090.6, "y": 2558.04, "z": 64.11}, {"x": 5091.01, "y": 2558.36, "z": 64.12}, {"x": 5100.0, "y": 2566.86, "z": 64.47}, {"x": 5100.0, "y": 2552.91, "z": 64.19}, {"x": 5095.83, "y": 2548.85, "z": 64.03}, {"x": 5091.83, "y": 2544.81, "z": 63.76}, {"x": 5091.55, "y": 2544.01, "z": 63.73}, {"x": 5091.62, "y": 2543.16, "z": 63.71}, {"x": 5094.28, "y": 2541.03, "z": 63.93}, {"x": 5085.8, "y": 2532.0, "z": 63.76}, {"x": 5082.86, "y": 2535.04, "z": 63.58}, {"x": 5082.26, "y": 2535.19, "z": 63.51}, {"x": 5081.6, "y": 2535.01, "z": 63.47}, {"x": 5080.9, "y": 2534.43, "z": 63.45}, {"x": 5080.0, "y": 2533.48, "z": 63.41}, {"x": 5078.98, "y": 2532.26, "z": 63.37}, {"x": 5078.14, "y": 2530.91, "z": 63.23}, {"x": 5077.24, "y": 2529.31, "z": 63.21}, {"x": 5076.66, "y": 2527.93, "z": 63.19}, {"x": 5076.34, "y": 2526.95, "z": 63.06}, {"x": 5076.14, "y": 2525.81, "z": 62.99}, {"x": 5075.89, "y": 2523.71, "z": 63.03}, {"x": 5075.78, "y": 2522.41, "z": 63.03}, {"x": 5075.78, "y": 2521.66, "z": 63.02}, {"x": 5075.79, "y": 2520.75, "z": 62.98}, {"x": 5075.92, "y": 2519.93, "z": 62.93}, {"x": 5076.18, "y": 2518.58, "z": 62.8}, {"x": 5076.65, "y": 2517.01, "z": 62.77}, {"x": 5077.06, "y": 2515.53, "z": 62.75}, {"x": 5077.4, "y": 2514.64, "z": 62.77}, {"x": 5077.96, "y": 2513.58, "z": 62.78}, {"x": 5078.69, "y": 2512.76, "z": 62.75}, {"x": 5080.14, "y": 2511.48, "z": 62.75}, {"x": 5083.35, "y": 2508.34, "z": 62.81}, {"x": 5093.71, "y": 2498.4, "z": 63.2}, {"x": 5090.57, "y": 2495.35, "z": 63.27}, {"x": 5081.65, "y": 2503.9, "z": 62.81}, {"x": 5080.92, "y": 2504.57, "z": 62.79}, {"x": 5080.52, "y": 2504.59, "z": 62.78}, {"x": 5080.15, "y": 2504.44, "z": 62.78}, {"x": 5079.95, "y": 2503.97, "z": 62.79}, {"x": 5079.99, "y": 2502.45, "z": 62.84}, {"x": 5079.62, "y": 2494.63, "z": 62.8}, {"x": 5079.39, "y": 2493.62, "z": 62.82}, {"x": 5079.29, "y": 2492.68, "z": 62.82}, {"x": 5079.32, "y": 2492.08, "z": 62.8}, {"x": 5079.69, "y": 2491.61, "z": 62.86}, {"x": 5080.79, "y": 2491.17, "z": 62.87}, {"x": 5083.13, "y": 2490.32, "z": 62.98}, {"x": 5090.31, "y": 2487.14, "z": 63.29}, {"x": 5095.81, "y": 2484.07, "z": 63.53}, {"x": 5098.57, "y": 2482.18, "z": 63.67}, {"x": 5100.95, "y": 2480.67, "z": 63.77}, {"x": 5104.65, "y": 2478.19, "z": 63.96}, {"x": 5107.23, "y": 2476.31, "z": 63.98}, {"x": 5112.11, "y": 2473.06, "z": 64.17}, {"x": 5113.22, "y": 2472.37, "z": 64.16}, {"x": 5115.44, "y": 2470.8, "z": 64.19}, {"x": 5116.4, "y": 2470.14, "z": 64.22}, {"x": 5116.58, "y": 2470.14, "z": 64.23}, {"x": 5116.68, "y": 2470.26, "z": 64.23}, {"x": 5116.64, "y": 2470.42, "z": 64.22}, {"x": 5116.53, "y": 2470.59, "z": 64.22}, {"x": 5116.2, "y": 2470.87, "z": 64.2}, {"x": 5113.51, "y": 2473.63, "z": 64.13}, {"x": 5116.98, "y": 2476.3, "z": 64.13}, {"x": 5130.0, "y": 2463.82, "z": 64.67}, {"x": 5130.0, "y": 2440.97, "z": 64.85}, {"x": 5129.86, "y": 2440.85, "z": 64.85}, {"x": 5129.72, "y": 2440.47, "z": 64.89}, {"x": 5129.55, "y": 2439.77, "z": 64.95}, {"x": 5125.08, "y": 2431.97, "z": 64.94}, {"x": 5119.65, "y": 2434.78, "z": 64.85}, {"x": 5122.66, "y": 2439.29, "z": 64.97}, {"x": 5122.9, "y": 2439.92, "z": 64.94}, {"x": 5122.86, "y": 2440.19, "z": 64.93}, {"x": 5122.61, "y": 2440.07, "z": 64.96}, {"x": 5122.21, "y": 2439.68, "z": 64.98}, {"x": 5119.0, "y": 2435.2, "z": 64.85}, {"x": 5115.25, "y": 2438.57, "z": 64.89}, {"x": 5117.72, "y": 2442.16, "z": 65.03}, {"x": 5120.91, "y": 2446.15, "z": 64.54}, {"x": 5121.28, "y": 2446.66, "z": 64.5}, {"x": 5121.3, "y": 2446.87, "z": 64.48}, {"x": 5121.09, "y": 2447.05, "z": 64.47}, {"x": 5120.85, "y": 2447.19, "z": 64.47}, {"x": 5114.69, "y": 2451.39, "z": 64.23}, {"x": 5105.83, "y": 2457.55, "z": 63.87}, {"x": 5101.82, "y": 2460.18, "z": 63.64}, {"x": 5098.84, "y": 2462.02, "z": 63.53}, {"x": 5095.63, "y": 2463.45, "z": 63.37}, {"x": 5093.17, "y": 2463.73, "z": 63.24}, {"x": 5090.35, "y": 2464.03, "z": 63.12}, {"x": 5086.92, "y": 2464.32, "z": 62.93}, {"x": 5078.37, "y": 2470.74, "z": 62.76}, {"x": 5082.09, "y": 2471.08, "z": 62.82}, {"x": 5082.52, "y": 2471.28, "z": 62.84}, {"x": 5082.64, "y": 2471.58, "z": 62.85}, {"x": 5082.68, "y": 2472.05, "z": 62.87}, {"x": 5082.46, "y": 2472.39, "z": 62.87}, {"x": 5077.98, "y": 2474.35, "z": 62.75}, {"x": 5072.97, "y": 2476.07, "z": 62.54}, {"x": 5071.95, "y": 2476.34, "z": 62.52}, {"x": 5071.32, "y": 2476.55, "z": 62.48}, {"x": 5070.79, "y": 2476.57, "z": 62.46}, {"x": 5070.31, "y": 2476.49, "z": 62.42}, {"x": 5069.76, "y": 2476.1, "z": 62.41}, {"x": 5065.87, "y": 2469.36, "z": 62.27}, {"x": 5065.24, "y": 2468.45, "z": 62.16}, {"x": 5065.26, "y": 2467.58, "z": 62.13}, {"x": 5066.28, "y": 2466.97, "z": 62.14}, {"x": 5070.14, "y": 2461.67, "z": 62.04}, {"x": 5068.37, "y": 2460.79, "z": 61.96}, {"x": 5065.93, "y": 2459.47, "z": 61.86}, {"x": 5063.28, "y": 2457.77, "z": 61.72}, {"x": 5061.62, "y": 2456.47, "z": 61.62}, {"x": 5060.36, "y": 2455.18, "z": 61.55}, {"x": 5059.46, "y": 2454.16, "z": 61.54}, {"x": 5049.97, "y": 2460.56, "z": 61.83}, {"x": 5051.95, "y": 2463.97, "z": 61.85}, {"x": 5054.18, "y": 2467.71, "z": 61.87}, {"x": 5054.77, "y": 2468.92, "z": 61.91}, {"x": 5054.53, "y": 2469.42, "z": 61.91}, {"x": 5054.02, "y": 2469.45, "z": 61.91}, {"x": 5053.65, "y": 2469.16, "z": 61.9}, {"x": 5052.29, "y": 2467.46, "z": 61.86}, {"x": 5045.62, "y": 2457.65, "z": 61.88}, {"x": 5040.42, "y": 2460.7, "z": 61.69}, {"x": 5045.85, "y": 2471.74, "z": 61.49}, {"x": 5046.59, "y": 2474.34, "z": 61.43}, {"x": 5046.69, "y": 2475.27, "z": 61.42}, {"x": 5046.67, "y": 2475.47, "z": 61.42}, {"x": 5046.49, "y": 2475.48, "z": 61.42}, {"x": 5045.75, "y": 2475.22, "z": 61.39}, {"x": 5044.45, "y": 2474.77, "z": 61.36}, {"x": 5038.78, "y": 2472.89, "z": 61.13}], "id": 1224872}, "1224856": {"area_boundary": [{"x": 5199.57, "y": 2529.87, "z": 67.0}, {"x": 5203.86, "y": 2525.95, "z": 67.11}, {"x": 5208.38, "y": 2521.71, "z": 67.2}, {"x": 5209.63, "y": 2520.48, "z": 67.22}, {"x": 5210.84, "y": 2519.43, "z": 67.24}, {"x": 5211.62, "y": 2518.98, "z": 67.26}, {"x": 5212.21, "y": 2518.97, "z": 67.26}, {"x": 5212.64, "y": 2519.22, "z": 67.28}, {"x": 5213.29, "y": 2519.94, "z": 67.29}, {"x": 5214.58, "y": 2521.78, "z": 67.35}, {"x": 5222.69, "y": 2533.31, "z": 67.64}, {"x": 5236.46, "y": 2552.62, "z": 68.05}, {"x": 5236.81, "y": 2553.16, "z": 68.05}, {"x": 5236.91, "y": 2553.66, "z": 68.06}, {"x": 5236.76, "y": 2554.07, "z": 68.07}, {"x": 5225.57, "y": 2564.27, "z": 68.08}, {"x": 5230.46, "y": 2569.79, "z": 68.17}, {"x": 5240.38, "y": 2560.52, "z": 68.24}, {"x": 5240.88, "y": 2560.09, "z": 68.22}, {"x": 5241.24, "y": 2559.96, "z": 68.22}, {"x": 5241.66, "y": 2560.02, "z": 68.22}, {"x": 5242.08, "y": 2560.25, "z": 68.22}, {"x": 5242.53, "y": 2560.78, "z": 68.24}, {"x": 5244.14, "y": 2563.17, "z": 68.27}, {"x": 5255.86, "y": 2579.29, "z": 68.63}, {"x": 5262.65, "y": 2574.86, "z": 68.76}, {"x": 5254.79, "y": 2563.93, "z": 68.51}, {"x": 5249.46, "y": 2556.5, "z": 68.36}, {"x": 5249.16, "y": 2556.08, "z": 68.31}, {"x": 5249.0, "y": 2555.67, "z": 68.31}, {"x": 5249.01, "y": 2555.19, "z": 68.32}, {"x": 5249.16, "y": 2554.67, "z": 68.32}, {"x": 5249.56, "y": 2553.98, "z": 68.35}, {"x": 5280.0, "y": 2525.9, "z": 69.68}, {"x": 5280.0, "y": 2518.26, "z": 69.7}, {"x": 5278.86, "y": 2519.33, "z": 69.67}, {"x": 5278.33, "y": 2519.39, "z": 69.66}, {"x": 5277.86, "y": 2519.27, "z": 69.65}, {"x": 5277.46, "y": 2518.96, "z": 69.63}, {"x": 5272.33, "y": 2512.39, "z": 69.46}, {"x": 5261.53, "y": 2498.96, "z": 69.21}, {"x": 5258.93, "y": 2501.21, "z": 69.24}, {"x": 5263.27, "y": 2507.46, "z": 69.34}, {"x": 5266.98, "y": 2513.23, "z": 69.42}, {"x": 5269.41, "y": 2516.8, "z": 69.5}, {"x": 5271.34, "y": 2519.12, "z": 69.55}, {"x": 5274.04, "y": 2522.49, "z": 69.54}, {"x": 5274.19, "y": 2522.95, "z": 69.54}, {"x": 5274.12, "y": 2523.49, "z": 69.51}, {"x": 5273.94, "y": 2523.89, "z": 69.48}, {"x": 5249.59, "y": 2546.91, "z": 68.47}, {"x": 5248.48, "y": 2547.77, "z": 68.43}, {"x": 5246.6, "y": 2548.97, "z": 68.27}, {"x": 5246.04, "y": 2549.2, "z": 68.23}, {"x": 5245.45, "y": 2549.32, "z": 68.19}, {"x": 5244.88, "y": 2549.26, "z": 68.17}, {"x": 5244.21, "y": 2548.96, "z": 68.15}, {"x": 5243.74, "y": 2548.49, "z": 68.16}, {"x": 5235.02, "y": 2536.19, "z": 67.92}, {"x": 5218.82, "y": 2513.84, "z": 67.41}, {"x": 5210.69, "y": 2502.5, "z": 67.13}, {"x": 5201.69, "y": 2490.0, "z": 66.87}, {"x": 5191.82, "y": 2490.0, "z": 66.62}, {"x": 5206.63, "y": 2510.74, "z": 67.09}, {"x": 5207.16, "y": 2511.57, "z": 67.1}, {"x": 5207.12, "y": 2512.27, "z": 67.11}, {"x": 5206.86, "y": 2513.0, "z": 67.11}, {"x": 5206.27, "y": 2513.59, "z": 67.12}, {"x": 5205.37, "y": 2514.31, "z": 67.14}, {"x": 5194.22, "y": 2524.62, "z": 66.88}], "id": 1224856}, "1224536": {"area_boundary": [{"x": 5030.01, "y": 2220.0, "z": 60.98}, {"x": 5020.71, "y": 2220.0, "z": 60.75}, {"x": 5022.66, "y": 2223.31, "z": 60.91}, {"x": 5047.06, "y": 2263.43, "z": 62.11}, {"x": 5047.38, "y": 2263.95, "z": 62.12}, {"x": 5047.53, "y": 2264.48, "z": 62.14}, {"x": 5047.44, "y": 2264.97, "z": 62.15}, {"x": 5047.15, "y": 2265.39, "z": 62.17}, {"x": 5046.74, "y": 2265.71, "z": 62.19}, {"x": 5044.02, "y": 2267.26, "z": 62.21}, {"x": 5043.77, "y": 2267.15, "z": 62.2}, {"x": 5043.38, "y": 2266.74, "z": 62.16}, {"x": 5042.92, "y": 2266.77, "z": 62.13}, {"x": 5023.67, "y": 2278.5, "z": 60.41}, {"x": 5021.7, "y": 2279.55, "z": 60.13}, {"x": 5020.64, "y": 2279.93, "z": 59.99}, {"x": 5019.7, "y": 2280.18, "z": 59.88}, {"x": 5018.72, "y": 2280.15, "z": 59.78}, {"x": 5018.03, "y": 2279.84, "z": 59.71}, {"x": 5017.43, "y": 2279.44, "z": 59.68}, {"x": 5016.86, "y": 2278.84, "z": 59.64}, {"x": 5016.19, "y": 2278.01, "z": 59.55}, {"x": 4999.75, "y": 2250.0, "z": 58.84}, {"x": 4992.92, "y": 2250.0, "z": 58.86}, {"x": 5011.6, "y": 2280.68, "z": 59.52}, {"x": 5011.63, "y": 2281.15, "z": 59.53}, {"x": 5011.48, "y": 2281.49, "z": 59.52}, {"x": 5010.79, "y": 2282.0, "z": 59.5}, {"x": 5009.73, "y": 2282.67, "z": 59.49}, {"x": 5004.83, "y": 2285.64, "z": 59.37}, {"x": 4988.93, "y": 2295.12, "z": 58.8}, {"x": 4990.2, "y": 2298.45, "z": 58.58}, {"x": 4990.2, "y": 2298.78, "z": 58.55}, {"x": 4990.07, "y": 2299.04, "z": 58.51}, {"x": 4989.24, "y": 2299.57, "z": 58.43}, {"x": 4988.73, "y": 2299.76, "z": 58.39}, {"x": 4988.35, "y": 2299.8, "z": 58.37}, {"x": 4987.95, "y": 2299.71, "z": 58.35}, {"x": 4987.6, "y": 2299.53, "z": 58.32}, {"x": 4987.28, "y": 2299.24, "z": 58.29}, {"x": 4969.42, "y": 2270.1, "z": 58.11}, {"x": 4957.26, "y": 2250.0, "z": 58.07}, {"x": 4949.58, "y": 2250.0, "z": 58.06}, {"x": 4964.17, "y": 2273.74, "z": 58.16}, {"x": 4970.14, "y": 2283.76, "z": 58.19}, {"x": 4981.92, "y": 2303.09, "z": 58.28}, {"x": 4983.28, "y": 2305.36, "z": 58.32}, {"x": 4983.69, "y": 2305.81, "z": 58.33}, {"x": 4984.23, "y": 2306.09, "z": 58.34}, {"x": 4984.91, "y": 2306.16, "z": 58.36}, {"x": 4985.5, "y": 2305.89, "z": 58.37}, {"x": 4987.7, "y": 2304.61, "z": 58.49}, {"x": 5046.16, "y": 2269.51, "z": 62.25}, {"x": 5047.08, "y": 2268.83, "z": 62.27}, {"x": 5048.45, "y": 2268.01, "z": 62.23}, {"x": 5048.87, "y": 2267.88, "z": 62.23}, {"x": 5049.33, "y": 2267.85, "z": 62.22}, {"x": 5049.68, "y": 2267.94, "z": 62.22}, {"x": 5049.98, "y": 2268.18, "z": 62.22}, {"x": 5055.99, "y": 2278.14, "z": 62.39}, {"x": 5066.04, "y": 2294.68, "z": 62.7}, {"x": 5069.43, "y": 2300.17, "z": 62.8}, {"x": 5079.4, "y": 2316.56, "z": 63.2}, {"x": 5092.18, "y": 2337.58, "z": 63.53}, {"x": 5103.73, "y": 2356.66, "z": 63.84}, {"x": 5111.89, "y": 2370.0, "z": 64.14}, {"x": 5121.31, "y": 2370.0, "z": 64.41}, {"x": 5111.53, "y": 2354.08, "z": 64.06}, {"x": 5094.14, "y": 2325.55, "z": 63.5}, {"x": 5086.44, "y": 2312.85, "z": 63.25}, {"x": 5076.96, "y": 2297.12, "z": 62.91}, {"x": 5076.69, "y": 2296.53, "z": 62.91}, {"x": 5076.74, "y": 2295.85, "z": 62.92}, {"x": 5076.87, "y": 2295.19, "z": 62.91}, {"x": 5077.09, "y": 2294.56, "z": 62.88}, {"x": 5077.53, "y": 2294.12, "z": 62.87}, {"x": 5100.0, "y": 2280.38, "z": 65.22}, {"x": 5100.0, "y": 2271.98, "z": 65.56}, {"x": 5073.16, "y": 2288.44, "z": 62.79}, {"x": 5072.7, "y": 2288.64, "z": 62.78}, {"x": 5072.2, "y": 2288.63, "z": 62.77}, {"x": 5071.82, "y": 2288.47, "z": 62.77}, {"x": 5071.57, "y": 2288.16, "z": 62.77}], "id": 1224536}, "1224530": {"area_boundary": [{"x": 5159.02, "y": 2247.95, "z": 71.45}, {"x": 5169.18, "y": 2264.82, "z": 71.1}, {"x": 5177.91, "y": 2279.1, "z": 70.76}, {"x": 5190.0, "y": 2299.06, "z": 70.41}, {"x": 5190.0, "y": 2287.64, "z": 70.64}, {"x": 5188.46, "y": 2285.2, "z": 70.69}, {"x": 5180.63, "y": 2272.35, "z": 70.95}, {"x": 5171.26, "y": 2256.75, "z": 71.26}, {"x": 5164.1, "y": 2244.91, "z": 71.5}, {"x": 5163.29, "y": 2243.55, "z": 71.54}, {"x": 5163.14, "y": 2243.14, "z": 71.55}, {"x": 5163.13, "y": 2242.81, "z": 71.56}, {"x": 5163.2, "y": 2242.41, "z": 71.57}, {"x": 5163.37, "y": 2242.07, "z": 71.58}, {"x": 5163.69, "y": 2241.78, "z": 71.59}, {"x": 5166.98, "y": 2239.71, "z": 71.7}, {"x": 5179.72, "y": 2231.94, "z": 72.09}, {"x": 5200.05, "y": 2219.53, "z": 72.67}, {"x": 5200.47, "y": 2219.39, "z": 72.68}, {"x": 5201.01, "y": 2219.41, "z": 72.68}, {"x": 5201.43, "y": 2219.67, "z": 72.71}, {"x": 5201.72, "y": 2220.04, "z": 72.73}, {"x": 5220.31, "y": 2250.0, "z": 72.46}, {"x": 5227.09, "y": 2250.0, "z": 72.42}, {"x": 5220.69, "y": 2240.03, "z": 72.56}, {"x": 5206.93, "y": 2217.38, "z": 72.86}, {"x": 5206.72, "y": 2217.01, "z": 72.86}, {"x": 5206.56, "y": 2216.67, "z": 72.85}, {"x": 5206.52, "y": 2216.26, "z": 72.85}, {"x": 5206.66, "y": 2215.86, "z": 72.86}, {"x": 5206.85, "y": 2215.53, "z": 72.87}, {"x": 5207.19, "y": 2215.27, "z": 72.89}, {"x": 5220.0, "y": 2207.43, "z": 73.28}, {"x": 5220.0, "y": 2199.21, "z": 73.5}, {"x": 5203.77, "y": 2209.03, "z": 72.92}, {"x": 5203.21, "y": 2209.34, "z": 72.91}, {"x": 5202.75, "y": 2209.46, "z": 72.89}, {"x": 5202.35, "y": 2209.37, "z": 72.88}, {"x": 5202.02, "y": 2209.09, "z": 72.88}, {"x": 5201.7, "y": 2208.61, "z": 72.89}, {"x": 5201.34, "y": 2207.92, "z": 72.88}, {"x": 5190.16, "y": 2190.0, "z": 72.36}, {"x": 5183.74, "y": 2190.0, "z": 72.26}, {"x": 5188.0, "y": 2197.2, "z": 72.51}, {"x": 5188.43, "y": 2197.06, "z": 72.5}, {"x": 5188.72, "y": 2197.19, "z": 72.49}, {"x": 5188.95, "y": 2197.49, "z": 72.5}, {"x": 5190.51, "y": 2200.13, "z": 72.58}, {"x": 5190.61, "y": 2200.42, "z": 72.59}, {"x": 5190.55, "y": 2200.68, "z": 72.6}, {"x": 5190.32, "y": 2201.04, "z": 72.61}, {"x": 5196.85, "y": 2211.92, "z": 72.75}, {"x": 5196.99, "y": 2212.33, "z": 72.74}, {"x": 5197.02, "y": 2212.79, "z": 72.71}, {"x": 5196.78, "y": 2213.22, "z": 72.7}, {"x": 5196.27, "y": 2213.57, "z": 72.69}, {"x": 5195.78, "y": 2213.85, "z": 72.67}, {"x": 5194.9, "y": 2214.34, "z": 72.66}, {"x": 5178.8, "y": 2224.17, "z": 72.18}, {"x": 5161.88, "y": 2234.51, "z": 71.68}, {"x": 5160.81, "y": 2235.1, "z": 71.65}, {"x": 5159.97, "y": 2235.55, "z": 71.62}, {"x": 5159.47, "y": 2235.61, "z": 71.6}, {"x": 5159.1, "y": 2235.63, "z": 71.58}, {"x": 5158.76, "y": 2235.55, "z": 71.57}, {"x": 5158.46, "y": 2235.38, "z": 71.56}, {"x": 5158.13, "y": 2235.07, "z": 71.53}, {"x": 5157.57, "y": 2234.27, "z": 71.53}, {"x": 5156.81, "y": 2233.0, "z": 71.47}, {"x": 5153.41, "y": 2227.49, "z": 71.12}, {"x": 5150.24, "y": 2222.29, "z": 70.77}, {"x": 5148.91, "y": 2220.0, "z": 70.65}, {"x": 5141.94, "y": 2220.0, "z": 70.48}, {"x": 5144.42, "y": 2224.07, "z": 70.75}, {"x": 5149.97, "y": 2233.1, "z": 71.28}, {"x": 5152.01, "y": 2236.54, "z": 71.47}, {"x": 5152.74, "y": 2237.7, "z": 71.5}, {"x": 5153.16, "y": 2238.48, "z": 71.51}, {"x": 5153.27, "y": 2238.91, "z": 71.51}, {"x": 5153.3, "y": 2239.25, "z": 71.51}, {"x": 5153.19, "y": 2239.53, "z": 71.49}, {"x": 5153.06, "y": 2239.69, "z": 71.47}, {"x": 5152.81, "y": 2239.91, "z": 71.46}, {"x": 5152.52, "y": 2240.09, "z": 71.44}, {"x": 5151.87, "y": 2240.47, "z": 71.39}, {"x": 5130.0, "y": 2253.72, "z": 68.74}, {"x": 5130.0, "y": 2262.26, "z": 68.28}, {"x": 5154.35, "y": 2247.33, "z": 71.28}, {"x": 5155.95, "y": 2246.42, "z": 71.41}, {"x": 5156.37, "y": 2246.18, "z": 71.43}, {"x": 5156.76, "y": 2246.03, "z": 71.45}, {"x": 5157.19, "y": 2245.97, "z": 71.46}, {"x": 5157.62, "y": 2246.07, "z": 71.47}, {"x": 5157.98, "y": 2246.34, "z": 71.48}, {"x": 5158.25, "y": 2246.66, "z": 71.48}, {"x": 5158.55, "y": 2247.12, "z": 71.47}], "id": 1224530}, "1224529": {"area_boundary": [{"x": 5364.74, "y": 2400.0, "z": 71.28}, {"x": 5348.92, "y": 2369.84, "z": 71.51}, {"x": 5347.72, "y": 2367.22, "z": 71.57}, {"x": 5347.81, "y": 2366.56, "z": 71.55}, {"x": 5348.45, "y": 2365.74, "z": 71.53}, {"x": 5349.76, "y": 2364.93, "z": 71.6}, {"x": 5370.0, "y": 2350.99, "z": 71.64}, {"x": 5370.0, "y": 2346.04, "z": 71.66}, {"x": 5346.12, "y": 2362.43, "z": 71.58}, {"x": 5345.43, "y": 2362.28, "z": 71.6}, {"x": 5344.82, "y": 2361.65, "z": 71.64}, {"x": 5326.71, "y": 2327.63, "z": 72.01}, {"x": 5326.18, "y": 2326.73, "z": 71.98}, {"x": 5325.92, "y": 2325.95, "z": 71.95}, {"x": 5325.89, "y": 2325.05, "z": 71.94}, {"x": 5326.18, "y": 2324.44, "z": 71.96}, {"x": 5326.92, "y": 2323.73, "z": 72.01}, {"x": 5340.0, "y": 2314.77, "z": 71.9}, {"x": 5340.0, "y": 2297.44, "z": 72.1}, {"x": 5325.64, "y": 2307.09, "z": 72.15}, {"x": 5325.08, "y": 2307.48, "z": 72.17}, {"x": 5324.63, "y": 2307.63, "z": 72.16}, {"x": 5324.09, "y": 2307.71, "z": 72.14}, {"x": 5323.46, "y": 2307.73, "z": 72.16}, {"x": 5322.85, "y": 2307.67, "z": 72.17}, {"x": 5322.41, "y": 2307.47, "z": 72.19}, {"x": 5321.7, "y": 2306.78, "z": 72.26}, {"x": 5319.92, "y": 2304.15, "z": 72.46}, {"x": 5309.54, "y": 2286.99, "z": 72.78}, {"x": 5302.77, "y": 2275.8, "z": 72.93}, {"x": 5291.83, "y": 2257.7, "z": 73.17}, {"x": 5287.46, "y": 2250.5, "z": 73.29}, {"x": 5287.41, "y": 2250.24, "z": 73.29}, {"x": 5287.43, "y": 2249.99, "z": 73.29}, {"x": 5287.51, "y": 2249.79, "z": 73.31}, {"x": 5287.77, "y": 2249.6, "z": 73.33}, {"x": 5292.98, "y": 2247.14, "z": 73.38}, {"x": 5288.96, "y": 2237.76, "z": 73.44}, {"x": 5283.04, "y": 2240.95, "z": 73.43}, {"x": 5282.55, "y": 2241.14, "z": 73.42}, {"x": 5282.21, "y": 2241.17, "z": 73.41}, {"x": 5281.97, "y": 2241.12, "z": 73.41}, {"x": 5281.79, "y": 2241.02, "z": 73.41}, {"x": 5281.61, "y": 2240.84, "z": 73.4}, {"x": 5279.01, "y": 2236.55, "z": 73.49}, {"x": 5273.12, "y": 2226.85, "z": 73.61}, {"x": 5272.68, "y": 2226.13, "z": 73.66}, {"x": 5272.51, "y": 2225.7, "z": 73.66}, {"x": 5272.5, "y": 2225.36, "z": 73.67}, {"x": 5272.62, "y": 2225.02, "z": 73.69}, {"x": 5272.92, "y": 2224.77, "z": 73.71}, {"x": 5276.72, "y": 2222.89, "z": 73.71}, {"x": 5275.57, "y": 2220.13, "z": 73.71}, {"x": 5270.46, "y": 2220.8, "z": 73.77}, {"x": 5269.87, "y": 2220.81, "z": 73.75}, {"x": 5269.64, "y": 2220.72, "z": 73.75}, {"x": 5269.44, "y": 2220.57, "z": 73.76}, {"x": 5269.2, "y": 2220.32, "z": 73.76}, {"x": 5268.97, "y": 2220.0, "z": 73.76}, {"x": 5260.63, "y": 2220.0, "z": 73.76}, {"x": 5260.68, "y": 2220.35, "z": 73.76}, {"x": 5260.74, "y": 2220.69, "z": 73.75}, {"x": 5260.74, "y": 2220.99, "z": 73.73}, {"x": 5260.62, "y": 2221.41, "z": 73.74}, {"x": 5260.32, "y": 2221.86, "z": 73.75}, {"x": 5259.27, "y": 2222.8, "z": 73.78}, {"x": 5256.07, "y": 2225.4, "z": 73.82}, {"x": 5262.44, "y": 2234.05, "z": 73.77}, {"x": 5266.19, "y": 2231.2, "z": 73.59}, {"x": 5266.65, "y": 2230.91, "z": 73.56}, {"x": 5267.0, "y": 2230.88, "z": 73.54}, {"x": 5267.25, "y": 2230.98, "z": 73.54}, {"x": 5267.45, "y": 2231.22, "z": 73.53}, {"x": 5270.88, "y": 2237.03, "z": 73.43}, {"x": 5278.1, "y": 2248.85, "z": 73.27}, {"x": 5284.55, "y": 2259.36, "z": 73.16}, {"x": 5293.45, "y": 2274.0, "z": 72.93}, {"x": 5296.37, "y": 2278.93, "z": 72.86}, {"x": 5296.66, "y": 2279.45, "z": 72.83}, {"x": 5296.85, "y": 2279.88, "z": 72.81}, {"x": 5296.88, "y": 2280.24, "z": 72.81}, {"x": 5296.8, "y": 2280.57, "z": 72.8}, {"x": 5296.61, "y": 2280.85, "z": 72.8}, {"x": 5296.14, "y": 2281.23, "z": 72.8}, {"x": 5294.97, "y": 2281.98, "z": 72.82}, {"x": 5296.48, "y": 2284.83, "z": 72.77}, {"x": 5296.97, "y": 2284.57, "z": 72.76}, {"x": 5297.47, "y": 2284.34, "z": 72.73}, {"x": 5297.85, "y": 2284.3, "z": 72.73}, {"x": 5298.26, "y": 2284.28, "z": 72.72}, {"x": 5298.69, "y": 2284.3, "z": 72.72}, {"x": 5299.17, "y": 2284.43, "z": 72.72}, {"x": 5299.65, "y": 2284.72, "z": 72.73}, {"x": 5300.1, "y": 2285.17, "z": 72.72}, {"x": 5300.42, "y": 2285.62, "z": 72.73}, {"x": 5300.84, "y": 2286.37, "z": 72.72}, {"x": 5313.12, "y": 2306.56, "z": 72.4}, {"x": 5315.43, "y": 2310.41, "z": 72.23}, {"x": 5316.06, "y": 2311.32, "z": 72.17}, {"x": 5316.31, "y": 2311.84, "z": 72.13}, {"x": 5316.44, "y": 2312.35, "z": 72.11}, {"x": 5316.43, "y": 2312.89, "z": 72.08}, {"x": 5316.19, "y": 2313.54, "z": 72.06}, {"x": 5315.74, "y": 2314.1, "z": 72.04}, {"x": 5314.22, "y": 2315.02, "z": 72.0}, {"x": 5281.75, "y": 2337.19, "z": 70.96}, {"x": 5280.92, "y": 2337.55, "z": 70.94}, {"x": 5280.38, "y": 2337.58, "z": 70.92}, {"x": 5279.83, "y": 2337.45, "z": 70.91}, {"x": 5279.45, "y": 2337.11, "z": 70.9}, {"x": 5278.95, "y": 2336.41, "z": 70.9}, {"x": 5274.25, "y": 2328.25, "z": 71.24}, {"x": 5269.43, "y": 2331.55, "z": 71.14}, {"x": 5272.96, "y": 2337.26, "z": 70.88}, {"x": 5274.72, "y": 2340.05, "z": 70.71}, {"x": 5274.95, "y": 2340.51, "z": 70.7}, {"x": 5274.94, "y": 2340.97, "z": 70.68}, {"x": 5274.82, "y": 2341.43, "z": 70.68}, {"x": 5274.61, "y": 2341.9, "z": 70.69}, {"x": 5274.21, "y": 2342.41, "z": 70.68}, {"x": 5250.55, "y": 2358.63, "z": 69.81}, {"x": 5240.68, "y": 2365.32, "z": 69.5}, {"x": 5240.0, "y": 2365.59, "z": 69.48}, {"x": 5239.44, "y": 2365.66, "z": 69.47}, {"x": 5238.94, "y": 2365.61, "z": 69.46}, {"x": 5238.44, "y": 2365.5, "z": 69.45}, {"x": 5238.03, "y": 2365.31, "z": 69.46}, {"x": 5237.57, "y": 2364.97, "z": 69.46}, {"x": 5237.2, "y": 2364.59, "z": 69.48}, {"x": 5236.83, "y": 2364.17, "z": 69.5}, {"x": 5236.44, "y": 2363.66, "z": 69.51}, {"x": 5219.81, "y": 2336.5, "z": 69.89}, {"x": 5219.71, "y": 2336.17, "z": 69.9}, {"x": 5219.78, "y": 2335.92, "z": 69.9}, {"x": 5220.03, "y": 2335.68, "z": 69.91}, {"x": 5226.56, "y": 2331.45, "z": 70.42}, {"x": 5224.76, "y": 2328.95, "z": 70.43}, {"x": 5218.66, "y": 2332.78, "z": 70.01}, {"x": 5218.35, "y": 2332.87, "z": 70.0}, {"x": 5218.02, "y": 2332.89, "z": 69.98}, {"x": 5217.74, "y": 2332.81, "z": 69.99}, {"x": 5217.42, "y": 2332.57, "z": 70.0}, {"x": 5217.01, "y": 2332.02, "z": 70.01}, {"x": 5190.0, "y": 2287.64, "z": 70.64}, {"x": 5190.0, "y": 2299.06, "z": 70.41}, {"x": 5193.57, "y": 2304.68, "z": 70.33}, {"x": 5209.32, "y": 2330.64, "z": 69.94}, {"x": 5209.6, "y": 2331.26, "z": 69.92}, {"x": 5209.7, "y": 2331.9, "z": 69.9}, {"x": 5209.65, "y": 2332.49, "z": 69.88}, {"x": 5209.5, "y": 2333.02, "z": 69.87}, {"x": 5209.32, "y": 2333.42, "z": 69.86}, {"x": 5209.05, "y": 2333.86, "z": 69.86}, {"x": 5208.63, "y": 2334.28, "z": 69.87}, {"x": 5207.73, "y": 2334.95, "z": 69.87}, {"x": 5177.12, "y": 2352.76, "z": 67.93}, {"x": 5173.1, "y": 2355.08, "z": 67.68}, {"x": 5172.33, "y": 2355.73, "z": 67.58}, {"x": 5171.54, "y": 2356.04, "z": 67.51}, {"x": 5171.04, "y": 2355.85, "z": 67.47}, {"x": 5170.53, "y": 2355.26, "z": 67.48}, {"x": 5161.34, "y": 2340.15, "z": 67.21}, {"x": 5157.57, "y": 2340.14, "z": 67.09}, {"x": 5166.72, "y": 2356.84, "z": 67.23}, {"x": 5167.58, "y": 2358.0, "z": 67.28}, {"x": 5167.61, "y": 2358.58, "z": 67.26}, {"x": 5167.36, "y": 2358.92, "z": 67.24}, {"x": 5166.7, "y": 2359.17, "z": 67.21}, {"x": 5165.75, "y": 2359.65, "z": 67.17}, {"x": 5148.62, "y": 2370.0, "z": 66.08}, {"x": 5160.02, "y": 2370.0, "z": 66.66}, {"x": 5170.43, "y": 2363.81, "z": 67.34}, {"x": 5170.61, "y": 2362.78, "z": 67.36}, {"x": 5170.95, "y": 2362.59, "z": 67.38}, {"x": 5171.4, "y": 2362.4, "z": 67.42}, {"x": 5171.86, "y": 2362.15, "z": 67.45}, {"x": 5191.39, "y": 2349.92, "z": 68.75}, {"x": 5209.53, "y": 2338.85, "z": 69.87}, {"x": 5210.95, "y": 2338.04, "z": 69.9}, {"x": 5211.69, "y": 2337.78, "z": 69.87}, {"x": 5212.5, "y": 2337.77, "z": 69.85}, {"x": 5213.05, "y": 2337.89, "z": 69.85}, {"x": 5213.68, "y": 2338.15, "z": 69.85}, {"x": 5214.18, "y": 2338.64, "z": 69.87}, {"x": 5214.63, "y": 2339.32, "z": 69.86}, {"x": 5232.17, "y": 2367.83, "z": 69.33}, {"x": 5232.45, "y": 2368.38, "z": 69.32}, {"x": 5232.55, "y": 2368.83, "z": 69.31}, {"x": 5232.52, "y": 2369.33, "z": 69.3}, {"x": 5232.37, "y": 2370.06, "z": 69.28}, {"x": 5232.15, "y": 2370.61, "z": 69.25}, {"x": 5231.8, "y": 2371.13, "z": 69.24}, {"x": 5231.31, "y": 2371.7, "z": 69.23}, {"x": 5230.53, "y": 2372.29, "z": 69.2}, {"x": 5220.0, "y": 2379.46, "z": 68.73}, {"x": 5220.0, "y": 2396.9, "z": 68.31}, {"x": 5231.35, "y": 2389.25, "z": 68.83}, {"x": 5231.75, "y": 2389.07, "z": 68.84}, {"x": 5232.33, "y": 2388.89, "z": 68.84}, {"x": 5232.89, "y": 2388.8, "z": 68.85}, {"x": 5233.47, "y": 2388.9, "z": 68.85}, {"x": 5234.03, "y": 2389.11, "z": 68.86}, {"x": 5234.48, "y": 2389.4, "z": 68.87}, {"x": 5234.84, "y": 2389.79, "z": 68.87}, {"x": 5235.23, "y": 2390.32, "z": 68.9}, {"x": 5235.54, "y": 2390.78, "z": 68.92}, {"x": 5235.83, "y": 2391.08, "z": 68.91}, {"x": 5236.09, "y": 2391.15, "z": 68.91}, {"x": 5237.21, "y": 2391.03, "z": 68.94}, {"x": 5237.56, "y": 2391.02, "z": 68.97}, {"x": 5237.79, "y": 2391.11, "z": 68.99}, {"x": 5244.23, "y": 2400.0, "z": 69.18}, {"x": 5251.14, "y": 2400.0, "z": 69.24}, {"x": 5241.35, "y": 2386.45, "z": 69.14}, {"x": 5240.81, "y": 2385.58, "z": 69.12}, {"x": 5240.58, "y": 2385.05, "z": 69.13}, {"x": 5240.48, "y": 2384.4, "z": 69.11}, {"x": 5240.58, "y": 2383.52, "z": 69.12}, {"x": 5240.75, "y": 2383.04, "z": 69.14}, {"x": 5241.07, "y": 2382.56, "z": 69.16}, {"x": 5241.62, "y": 2382.07, "z": 69.22}, {"x": 5250.46, "y": 2376.03, "z": 69.62}, {"x": 5259.13, "y": 2369.98, "z": 69.9}, {"x": 5280.86, "y": 2355.18, "z": 70.65}, {"x": 5313.37, "y": 2332.91, "z": 71.78}, {"x": 5316.35, "y": 2331.01, "z": 71.84}, {"x": 5317.04, "y": 2330.65, "z": 71.84}, {"x": 5317.63, "y": 2330.41, "z": 71.83}, {"x": 5318.37, "y": 2330.4, "z": 71.83}, {"x": 5319.14, "y": 2330.69, "z": 71.84}, {"x": 5319.86, "y": 2331.41, "z": 71.85}, {"x": 5320.99, "y": 2333.77, "z": 71.87}, {"x": 5337.15, "y": 2364.58, "z": 71.71}, {"x": 5338.04, "y": 2366.29, "z": 71.66}, {"x": 5337.97, "y": 2366.9, "z": 71.65}, {"x": 5337.57, "y": 2367.32, "z": 71.66}, {"x": 5335.67, "y": 2368.8, "z": 71.67}, {"x": 5290.94, "y": 2400.19, "z": 70.32}, {"x": 5280.0, "y": 2407.51, "z": 70.05}, {"x": 5280.0, "y": 2413.56, "z": 70.11}, {"x": 5297.38, "y": 2401.89, "z": 70.53}, {"x": 5297.66, "y": 2401.71, "z": 70.53}, {"x": 5297.98, "y": 2401.68, "z": 70.55}, {"x": 5298.33, "y": 2401.69, "z": 70.53}, {"x": 5298.65, "y": 2401.74, "z": 70.51}, {"x": 5299.22, "y": 2402.42, "z": 70.52}, {"x": 5311.88, "y": 2419.73, "z": 70.46}, {"x": 5316.7, "y": 2419.79, "z": 70.48}, {"x": 5302.78, "y": 2400.09, "z": 70.56}, {"x": 5302.04, "y": 2398.98, "z": 70.56}, {"x": 5302.02, "y": 2398.79, "z": 70.56}, {"x": 5302.1, "y": 2398.58, "z": 70.57}, {"x": 5302.24, "y": 2398.41, "z": 70.58}, {"x": 5313.36, "y": 2390.94, "z": 71.07}, {"x": 5313.64, "y": 2390.55, "z": 71.07}, {"x": 5313.95, "y": 2390.16, "z": 71.06}, {"x": 5314.42, "y": 2389.98, "z": 71.08}, {"x": 5314.96, "y": 2389.97, "z": 71.11}, {"x": 5320.84, "y": 2385.94, "z": 71.21}, {"x": 5337.84, "y": 2373.86, "z": 71.66}, {"x": 5340.12, "y": 2372.61, "z": 71.64}, {"x": 5340.91, "y": 2372.57, "z": 71.63}, {"x": 5341.4, "y": 2372.75, "z": 71.61}, {"x": 5342.89, "y": 2375.42, "z": 71.55}, {"x": 5355.85, "y": 2400.0, "z": 71.25}], "id": 1224529}, "1224509": {"area_boundary": [{"x": 5042.53, "y": 2366.6, "z": 60.48}, {"x": 5048.95, "y": 2376.56, "z": 60.7}, {"x": 5049.11, "y": 2376.86, "z": 60.7}, {"x": 5049.08, "y": 2377.14, "z": 60.7}, {"x": 5048.85, "y": 2377.43, "z": 60.71}, {"x": 5041.84, "y": 2381.45, "z": 60.68}, {"x": 5044.24, "y": 2386.2, "z": 60.82}, {"x": 5052.43, "y": 2382.42, "z": 60.93}, {"x": 5052.76, "y": 2382.41, "z": 60.93}, {"x": 5052.98, "y": 2382.56, "z": 60.95}, {"x": 5055.79, "y": 2386.62, "z": 61.16}, {"x": 5057.12, "y": 2388.01, "z": 61.24}, {"x": 5064.28, "y": 2398.88, "z": 61.68}, {"x": 5078.72, "y": 2421.18, "z": 62.1}, {"x": 5080.61, "y": 2424.01, "z": 62.16}, {"x": 5080.68, "y": 2424.34, "z": 62.16}, {"x": 5080.21, "y": 2425.35, "z": 62.14}, {"x": 5079.94, "y": 2425.78, "z": 62.12}, {"x": 5079.7, "y": 2426.08, "z": 62.12}, {"x": 5068.61, "y": 2433.57, "z": 61.51}, {"x": 5063.29, "y": 2437.81, "z": 61.16}, {"x": 5066.87, "y": 2439.45, "z": 61.29}, {"x": 5071.98, "y": 2436.17, "z": 61.59}, {"x": 5089.79, "y": 2424.19, "z": 62.53}, {"x": 5107.22, "y": 2412.35, "z": 63.55}, {"x": 5125.13, "y": 2400.0, "z": 64.58}, {"x": 5117.72, "y": 2400.0, "z": 64.3}, {"x": 5103.53, "y": 2409.67, "z": 63.5}, {"x": 5091.22, "y": 2418.13, "z": 62.78}, {"x": 5087.62, "y": 2420.55, "z": 62.53}, {"x": 5087.08, "y": 2420.93, "z": 62.46}, {"x": 5086.61, "y": 2421.09, "z": 62.42}, {"x": 5086.26, "y": 2421.18, "z": 62.39}, {"x": 5085.84, "y": 2421.16, "z": 62.37}, {"x": 5085.53, "y": 2421.09, "z": 62.35}, {"x": 5085.26, "y": 2420.92, "z": 62.32}, {"x": 5084.98, "y": 2420.46, "z": 62.31}, {"x": 5079.22, "y": 2411.45, "z": 62.14}, {"x": 5061.01, "y": 2383.65, "z": 61.26}, {"x": 5058.92, "y": 2380.54, "z": 61.13}, {"x": 5058.09, "y": 2379.47, "z": 61.07}, {"x": 5056.23, "y": 2376.61, "z": 60.91}, {"x": 5055.49, "y": 2375.57, "z": 60.85}, {"x": 5051.0, "y": 2368.54, "z": 60.62}, {"x": 5047.63, "y": 2363.08, "z": 60.59}], "id": 1224509}, "1224499": {"area_boundary": [{"x": 5220.0, "y": 2448.31, "z": 68.12}, {"x": 5195.83, "y": 2464.77, "z": 66.86}, {"x": 5193.04, "y": 2465.78, "z": 66.8}, {"x": 5188.68, "y": 2468.75, "z": 66.52}, {"x": 5187.57, "y": 2469.36, "z": 66.49}, {"x": 5187.18, "y": 2469.45, "z": 66.48}, {"x": 5186.86, "y": 2469.41, "z": 66.47}, {"x": 5186.63, "y": 2469.25, "z": 66.47}, {"x": 5185.06, "y": 2467.04, "z": 66.43}, {"x": 5183.61, "y": 2464.83, "z": 66.42}, {"x": 5178.04, "y": 2457.02, "z": 66.19}, {"x": 5169.09, "y": 2444.5, "z": 65.98}, {"x": 5165.82, "y": 2440.03, "z": 66.0}, {"x": 5165.28, "y": 2439.13, "z": 65.94}, {"x": 5164.83, "y": 2438.0, "z": 65.96}, {"x": 5164.66, "y": 2437.07, "z": 65.93}, {"x": 5164.72, "y": 2435.69, "z": 65.92}, {"x": 5165.27, "y": 2434.78, "z": 65.99}, {"x": 5166.8, "y": 2433.46, "z": 66.08}, {"x": 5170.51, "y": 2430.82, "z": 66.28}, {"x": 5206.01, "y": 2406.55, "z": 67.74}, {"x": 5215.51, "y": 2399.98, "z": 68.12}, {"x": 5220.0, "y": 2396.9, "z": 68.31}, {"x": 5220.0, "y": 2379.46, "z": 68.73}, {"x": 5200.44, "y": 2392.72, "z": 67.86}, {"x": 5193.52, "y": 2397.52, "z": 67.53}, {"x": 5173.62, "y": 2411.12, "z": 66.71}, {"x": 5156.57, "y": 2422.6, "z": 65.98}, {"x": 5156.03, "y": 2422.83, "z": 65.95}, {"x": 5155.44, "y": 2422.97, "z": 65.92}, {"x": 5154.98, "y": 2422.94, "z": 65.9}, {"x": 5154.43, "y": 2422.77, "z": 65.87}, {"x": 5153.73, "y": 2422.38, "z": 65.83}, {"x": 5153.22, "y": 2422.01, "z": 65.79}, {"x": 5152.41, "y": 2420.98, "z": 65.76}, {"x": 5132.83, "y": 2388.88, "z": 64.96}, {"x": 5132.59, "y": 2388.35, "z": 64.93}, {"x": 5132.4, "y": 2387.73, "z": 64.89}, {"x": 5132.36, "y": 2387.24, "z": 64.88}, {"x": 5132.35, "y": 2386.79, "z": 64.86}, {"x": 5132.51, "y": 2386.28, "z": 64.86}, {"x": 5132.81, "y": 2385.87, "z": 64.87}, {"x": 5133.24, "y": 2385.46, "z": 64.88}, {"x": 5133.78, "y": 2385.1, "z": 64.92}, {"x": 5143.52, "y": 2379.06, "z": 65.53}, {"x": 5150.17, "y": 2376.35, "z": 66.08}, {"x": 5160.02, "y": 2370.0, "z": 66.66}, {"x": 5148.62, "y": 2370.0, "z": 66.08}, {"x": 5132.52, "y": 2379.7, "z": 64.99}, {"x": 5132.28, "y": 2381.0, "z": 64.91}, {"x": 5131.19, "y": 2381.85, "z": 64.82}, {"x": 5129.93, "y": 2382.62, "z": 64.79}, {"x": 5129.66, "y": 2382.68, "z": 64.78}, {"x": 5129.35, "y": 2382.66, "z": 64.78}, {"x": 5129.05, "y": 2382.5, "z": 64.78}, {"x": 5128.73, "y": 2382.21, "z": 64.77}, {"x": 5121.31, "y": 2370.0, "z": 64.41}, {"x": 5111.89, "y": 2370.0, "z": 64.14}, {"x": 5125.93, "y": 2392.88, "z": 64.68}, {"x": 5126.06, "y": 2393.27, "z": 64.68}, {"x": 5126.09, "y": 2393.71, "z": 64.68}, {"x": 5126.07, "y": 2393.94, "z": 64.67}, {"x": 5126.01, "y": 2394.18, "z": 64.67}, {"x": 5125.88, "y": 2394.43, "z": 64.68}, {"x": 5125.67, "y": 2394.65, "z": 64.68}, {"x": 5117.72, "y": 2400.0, "z": 64.3}, {"x": 5125.13, "y": 2400.0, "z": 64.58}, {"x": 5126.69, "y": 2398.93, "z": 64.71}, {"x": 5127.16, "y": 2398.75, "z": 64.72}, {"x": 5127.74, "y": 2398.67, "z": 64.73}, {"x": 5128.25, "y": 2398.63, "z": 64.74}, {"x": 5128.85, "y": 2398.82, "z": 64.77}, {"x": 5129.29, "y": 2399.14, "z": 64.79}, {"x": 5129.76, "y": 2399.54, "z": 64.83}, {"x": 5130.09, "y": 2399.92, "z": 64.84}, {"x": 5145.96, "y": 2425.82, "z": 65.61}, {"x": 5146.29, "y": 2426.52, "z": 65.6}, {"x": 5146.51, "y": 2427.24, "z": 65.6}, {"x": 5146.59, "y": 2427.88, "z": 65.59}, {"x": 5146.53, "y": 2428.81, "z": 65.58}, {"x": 5146.17, "y": 2429.6, "z": 65.57}, {"x": 5145.59, "y": 2430.3, "z": 65.57}, {"x": 5142.08, "y": 2432.68, "z": 65.42}, {"x": 5130.0, "y": 2440.97, "z": 64.85}, {"x": 5130.0, "y": 2463.82, "z": 64.67}, {"x": 5143.84, "y": 2450.6, "z": 65.13}, {"x": 5147.97, "y": 2447.04, "z": 65.29}, {"x": 5154.25, "y": 2442.73, "z": 65.52}, {"x": 5155.75, "y": 2442.49, "z": 65.54}, {"x": 5157.03, "y": 2442.76, "z": 65.56}, {"x": 5158.13, "y": 2443.33, "z": 65.63}, {"x": 5158.73, "y": 2443.96, "z": 65.67}, {"x": 5172.99, "y": 2463.63, "z": 66.18}, {"x": 5182.31, "y": 2476.87, "z": 66.34}, {"x": 5182.67, "y": 2477.52, "z": 66.35}, {"x": 5182.74, "y": 2477.98, "z": 66.34}, {"x": 5182.65, "y": 2478.35, "z": 66.38}, {"x": 5182.35, "y": 2478.83, "z": 66.43}, {"x": 5180.21, "y": 2480.96, "z": 66.64}, {"x": 5183.5, "y": 2485.46, "z": 66.72}, {"x": 5186.27, "y": 2483.84, "z": 66.58}, {"x": 5186.71, "y": 2483.79, "z": 66.54}, {"x": 5187.07, "y": 2483.83, "z": 66.52}, {"x": 5187.33, "y": 2483.95, "z": 66.52}, {"x": 5187.71, "y": 2484.41, "z": 66.53}, {"x": 5191.82, "y": 2490.0, "z": 66.62}, {"x": 5201.69, "y": 2490.0, "z": 66.87}, {"x": 5191.41, "y": 2475.75, "z": 66.57}, {"x": 5191.17, "y": 2475.29, "z": 66.55}, {"x": 5191.11, "y": 2474.94, "z": 66.54}, {"x": 5191.22, "y": 2474.57, "z": 66.55}, {"x": 5191.51, "y": 2474.24, "z": 66.57}, {"x": 5192.73, "y": 2473.28, "z": 66.59}, {"x": 5193.35, "y": 2472.73, "z": 66.62}, {"x": 5220.0, "y": 2454.47, "z": 67.99}], "id": 1224499}, "1224498": {"area_boundary": [{"x": 5104.53, "y": 2488.15, "z": 63.66}, {"x": 5110.51, "y": 2482.47, "z": 63.9}, {"x": 5116.98, "y": 2476.3, "z": 64.13}, {"x": 5113.51, "y": 2473.63, "z": 64.13}, {"x": 5110.03, "y": 2476.77, "z": 63.99}, {"x": 5107.1, "y": 2479.57, "z": 63.95}, {"x": 5101.3, "y": 2485.1, "z": 63.73}, {"x": 5095.12, "y": 2491.1, "z": 63.43}, {"x": 5090.57, "y": 2495.35, "z": 63.27}, {"x": 5093.71, "y": 2498.4, "z": 63.2}], "id": 1224498}, "1224493": {"area_boundary": [{"x": 5355.85, "y": 2400.0, "z": 71.25}, {"x": 5360.17, "y": 2408.6, "z": 71.27}, {"x": 5363.42, "y": 2414.72, "z": 71.24}, {"x": 5369.47, "y": 2426.02, "z": 71.09}, {"x": 5372.23, "y": 2431.62, "z": 71.02}, {"x": 5372.62, "y": 2432.34, "z": 71.0}, {"x": 5372.68, "y": 2432.9, "z": 70.99}, {"x": 5372.44, "y": 2433.28, "z": 70.99}, {"x": 5371.98, "y": 2433.75, "z": 71.0}, {"x": 5370.83, "y": 2434.75, "z": 71.03}, {"x": 5372.65, "y": 2438.8, "z": 71.04}, {"x": 5374.24, "y": 2437.41, "z": 70.99}, {"x": 5374.65, "y": 2437.21, "z": 70.97}, {"x": 5375.16, "y": 2437.29, "z": 70.97}, {"x": 5375.56, "y": 2437.67, "z": 70.98}, {"x": 5381.42, "y": 2448.81, "z": 70.93}, {"x": 5388.94, "y": 2463.05, "z": 70.87}, {"x": 5393.16, "y": 2471.05, "z": 70.81}, {"x": 5394.28, "y": 2473.43, "z": 70.8}, {"x": 5394.45, "y": 2474.03, "z": 70.8}, {"x": 5394.5, "y": 2474.57, "z": 70.79}, {"x": 5394.45, "y": 2475.1, "z": 70.77}, {"x": 5394.26, "y": 2475.68, "z": 70.77}, {"x": 5393.62, "y": 2476.56, "z": 70.78}, {"x": 5391.71, "y": 2478.23, "z": 70.73}, {"x": 5381.97, "y": 2487.32, "z": 70.51}, {"x": 5381.51, "y": 2487.33, "z": 70.51}, {"x": 5381.09, "y": 2487.1, "z": 70.55}, {"x": 5377.74, "y": 2482.89, "z": 70.81}, {"x": 5372.64, "y": 2486.92, "z": 70.67}, {"x": 5377.0, "y": 2490.69, "z": 70.47}, {"x": 5377.24, "y": 2491.27, "z": 70.43}, {"x": 5377.13, "y": 2491.77, "z": 70.38}, {"x": 5370.69, "y": 2497.7, "z": 70.3}, {"x": 5359.62, "y": 2507.96, "z": 70.07}, {"x": 5348.06, "y": 2518.66, "z": 69.88}, {"x": 5343.61, "y": 2522.76, "z": 69.81}, {"x": 5343.03, "y": 2523.17, "z": 69.8}, {"x": 5342.38, "y": 2523.53, "z": 69.78}, {"x": 5341.67, "y": 2523.65, "z": 69.76}, {"x": 5340.95, "y": 2523.64, "z": 69.73}, {"x": 5340.22, "y": 2523.48, "z": 69.72}, {"x": 5339.65, "y": 2523.1, "z": 69.71}, {"x": 5334.4, "y": 2516.27, "z": 69.78}, {"x": 5329.42, "y": 2509.12, "z": 69.83}, {"x": 5317.22, "y": 2491.93, "z": 69.86}, {"x": 5316.87, "y": 2491.45, "z": 69.88}, {"x": 5316.82, "y": 2490.82, "z": 69.89}, {"x": 5317.2, "y": 2490.24, "z": 69.88}, {"x": 5317.95, "y": 2489.49, "z": 69.9}, {"x": 5319.25, "y": 2488.26, "z": 69.94}, {"x": 5316.71, "y": 2484.89, "z": 69.92}, {"x": 5314.91, "y": 2486.55, "z": 69.88}, {"x": 5314.52, "y": 2486.78, "z": 69.88}, {"x": 5314.29, "y": 2486.84, "z": 69.88}, {"x": 5313.85, "y": 2486.84, "z": 69.87}, {"x": 5313.56, "y": 2486.72, "z": 69.88}, {"x": 5313.18, "y": 2486.36, "z": 69.89}, {"x": 5312.85, "y": 2486.0, "z": 69.9}, {"x": 5267.71, "y": 2423.07, "z": 69.62}, {"x": 5267.47, "y": 2422.59, "z": 69.58}, {"x": 5267.45, "y": 2422.32, "z": 69.57}, {"x": 5267.5, "y": 2422.05, "z": 69.57}, {"x": 5267.68, "y": 2421.81, "z": 69.57}, {"x": 5270.3, "y": 2420.15, "z": 69.72}, {"x": 5280.0, "y": 2413.56, "z": 70.11}, {"x": 5280.0, "y": 2407.51, "z": 70.05}, {"x": 5267.88, "y": 2415.46, "z": 69.69}, {"x": 5265.17, "y": 2416.82, "z": 69.56}, {"x": 5264.52, "y": 2417.22, "z": 69.51}, {"x": 5263.9, "y": 2417.28, "z": 69.5}, {"x": 5263.44, "y": 2417.08, "z": 69.48}, {"x": 5263.07, "y": 2416.65, "z": 69.49}, {"x": 5251.14, "y": 2400.0, "z": 69.24}, {"x": 5244.23, "y": 2400.0, "z": 69.18}, {"x": 5247.68, "y": 2404.74, "z": 69.27}, {"x": 5246.27, "y": 2405.95, "z": 69.3}, {"x": 5256.62, "y": 2420.31, "z": 69.51}, {"x": 5257.0, "y": 2420.85, "z": 69.5}, {"x": 5257.08, "y": 2421.48, "z": 69.49}, {"x": 5256.89, "y": 2422.04, "z": 69.49}, {"x": 5256.53, "y": 2422.43, "z": 69.5}, {"x": 5255.68, "y": 2423.01, "z": 69.51}, {"x": 5256.0, "y": 2423.6, "z": 69.51}, {"x": 5255.23, "y": 2424.09, "z": 69.53}, {"x": 5254.78, "y": 2423.46, "z": 69.54}, {"x": 5229.07, "y": 2441.71, "z": 68.45}, {"x": 5220.0, "y": 2448.31, "z": 68.12}, {"x": 5220.0, "y": 2454.47, "z": 67.99}, {"x": 5223.55, "y": 2452.05, "z": 68.17}, {"x": 5223.78, "y": 2452.02, "z": 68.19}, {"x": 5224.07, "y": 2452.09, "z": 68.19}, {"x": 5224.37, "y": 2452.3, "z": 68.21}, {"x": 5228.43, "y": 2458.41, "z": 68.39}, {"x": 5232.62, "y": 2455.82, "z": 68.4}, {"x": 5228.19, "y": 2449.59, "z": 68.32}, {"x": 5228.09, "y": 2449.26, "z": 68.32}, {"x": 5228.11, "y": 2449.03, "z": 68.32}, {"x": 5228.26, "y": 2448.83, "z": 68.32}, {"x": 5241.14, "y": 2440.02, "z": 68.87}, {"x": 5257.81, "y": 2428.43, "z": 69.61}, {"x": 5259.2, "y": 2427.52, "z": 69.59}, {"x": 5260.44, "y": 2426.68, "z": 69.53}, {"x": 5260.75, "y": 2426.65, "z": 69.54}, {"x": 5261.19, "y": 2426.88, "z": 69.56}, {"x": 5261.48, "y": 2427.25, "z": 69.57}, {"x": 5273.93, "y": 2444.61, "z": 69.86}, {"x": 5273.93, "y": 2444.96, "z": 69.87}, {"x": 5269.26, "y": 2448.15, "z": 69.95}, {"x": 5272.94, "y": 2453.58, "z": 69.91}, {"x": 5277.82, "y": 2450.39, "z": 69.96}, {"x": 5278.09, "y": 2450.44, "z": 69.96}, {"x": 5306.82, "y": 2490.42, "z": 69.87}, {"x": 5307.18, "y": 2490.92, "z": 69.87}, {"x": 5307.34, "y": 2491.31, "z": 69.87}, {"x": 5307.4, "y": 2491.66, "z": 69.88}, {"x": 5307.33, "y": 2492.09, "z": 69.9}, {"x": 5307.08, "y": 2492.58, "z": 69.91}, {"x": 5306.43, "y": 2493.36, "z": 69.95}, {"x": 5291.89, "y": 2506.64, "z": 70.01}, {"x": 5280.0, "y": 2518.26, "z": 69.7}, {"x": 5280.0, "y": 2525.9, "z": 69.68}, {"x": 5309.49, "y": 2498.45, "z": 69.96}, {"x": 5310.15, "y": 2497.99, "z": 69.93}, {"x": 5310.69, "y": 2497.75, "z": 69.92}, {"x": 5311.29, "y": 2497.7, "z": 69.91}, {"x": 5311.94, "y": 2497.86, "z": 69.89}, {"x": 5312.36, "y": 2498.15, "z": 69.89}, {"x": 5312.72, "y": 2498.56, "z": 69.89}, {"x": 5323.83, "y": 2514.0, "z": 69.8}, {"x": 5332.91, "y": 2526.68, "z": 69.74}, {"x": 5334.22, "y": 2528.39, "z": 69.64}, {"x": 5334.4, "y": 2529.2, "z": 69.63}, {"x": 5334.46, "y": 2530.05, "z": 69.65}, {"x": 5334.34, "y": 2530.78, "z": 69.66}, {"x": 5334.15, "y": 2531.38, "z": 69.67}, {"x": 5331.75, "y": 2533.79, "z": 69.66}, {"x": 5317.18, "y": 2547.3, "z": 69.44}, {"x": 5310.0, "y": 2553.85, "z": 69.32}, {"x": 5310.0, "y": 2564.81, "z": 69.37}, {"x": 5321.11, "y": 2554.59, "z": 69.49}, {"x": 5322.9, "y": 2553.07, "z": 69.48}, {"x": 5323.55, "y": 2552.73, "z": 69.48}, {"x": 5324.25, "y": 2552.65, "z": 69.5}, {"x": 5324.87, "y": 2552.8, "z": 69.55}, {"x": 5325.44, "y": 2553.1, "z": 69.58}, {"x": 5325.96, "y": 2553.68, "z": 69.6}, {"x": 5350.83, "y": 2580.0, "z": 69.59}, {"x": 5356.31, "y": 2580.0, "z": 69.61}, {"x": 5346.15, "y": 2569.22, "z": 69.55}, {"x": 5345.83, "y": 2568.77, "z": 69.54}, {"x": 5345.68, "y": 2568.28, "z": 69.53}, {"x": 5345.64, "y": 2567.72, "z": 69.52}, {"x": 5345.68, "y": 2567.22, "z": 69.52}, {"x": 5345.88, "y": 2566.8, "z": 69.53}, {"x": 5346.4, "y": 2566.39, "z": 69.51}, {"x": 5370.0, "y": 2555.95, "z": 69.93}, {"x": 5370.0, "y": 2551.54, "z": 69.88}, {"x": 5360.46, "y": 2555.88, "z": 69.74}, {"x": 5351.67, "y": 2560.2, "z": 69.54}, {"x": 5344.38, "y": 2564.1, "z": 69.49}, {"x": 5343.75, "y": 2564.33, "z": 69.51}, {"x": 5343.13, "y": 2564.47, "z": 69.53}, {"x": 5342.48, "y": 2564.47, "z": 69.54}, {"x": 5341.98, "y": 2564.36, "z": 69.57}, {"x": 5341.59, "y": 2564.12, "z": 69.55}, {"x": 5329.06, "y": 2550.7, "z": 69.58}, {"x": 5328.74, "y": 2550.18, "z": 69.6}, {"x": 5328.48, "y": 2549.63, "z": 69.59}, {"x": 5328.29, "y": 2549.15, "z": 69.57}, {"x": 5328.26, "y": 2548.56, "z": 69.55}, {"x": 5328.46, "y": 2547.99, "z": 69.55}, {"x": 5328.81, "y": 2547.5, "z": 69.56}, {"x": 5334.43, "y": 2542.36, "z": 69.65}, {"x": 5348.27, "y": 2529.54, "z": 69.87}, {"x": 5362.36, "y": 2516.42, "z": 70.14}, {"x": 5368.75, "y": 2510.66, "z": 70.23}, {"x": 5369.4, "y": 2510.51, "z": 70.23}, {"x": 5370.1, "y": 2510.49, "z": 70.23}, {"x": 5370.7, "y": 2510.76, "z": 70.26}, {"x": 5371.09, "y": 2511.16, "z": 70.28}, {"x": 5371.43, "y": 2511.56, "z": 70.3}, {"x": 5373.21, "y": 2515.71, "z": 70.31}, {"x": 5375.04, "y": 2520.0, "z": 70.31}, {"x": 5379.62, "y": 2520.0, "z": 70.28}, {"x": 5374.85, "y": 2508.68, "z": 70.33}, {"x": 5374.05, "y": 2507.53, "z": 70.28}, {"x": 5373.89, "y": 2506.89, "z": 70.28}, {"x": 5374.03, "y": 2506.09, "z": 70.3}, {"x": 5374.36, "y": 2505.45, "z": 70.33}, {"x": 5374.87, "y": 2504.87, "z": 70.39}, {"x": 5385.15, "y": 2495.42, "z": 70.54}, {"x": 5387.59, "y": 2493.15, "z": 70.61}, {"x": 5388.61, "y": 2492.14, "z": 70.63}, {"x": 5395.95, "y": 2485.31, "z": 70.77}, {"x": 5397.0, "y": 2484.67, "z": 70.82}, {"x": 5397.71, "y": 2484.46, "z": 70.8}, {"x": 5398.33, "y": 2484.45, "z": 70.79}, {"x": 5399.0, "y": 2484.52, "z": 70.8}, {"x": 5399.63, "y": 2484.77, "z": 70.81}, {"x": 5400.32, "y": 2485.27, "z": 70.84}, {"x": 5400.72, "y": 2485.72, "z": 70.86}, {"x": 5402.28, "y": 2488.82, "z": 70.9}, {"x": 5408.84, "y": 2502.95, "z": 70.76}, {"x": 5415.47, "y": 2517.1, "z": 70.76}, {"x": 5416.83, "y": 2520.0, "z": 70.71}, {"x": 5425.39, "y": 2520.0, "z": 70.82}, {"x": 5424.9, "y": 2518.75, "z": 70.83}, {"x": 5420.24, "y": 2508.82, "z": 70.86}, {"x": 5414.09, "y": 2495.61, "z": 70.87}, {"x": 5408.39, "y": 2483.25, "z": 70.97}, {"x": 5406.66, "y": 2479.59, "z": 70.96}, {"x": 5406.61, "y": 2479.17, "z": 70.96}, {"x": 5406.57, "y": 2478.54, "z": 70.95}, {"x": 5406.6, "y": 2477.93, "z": 70.95}, {"x": 5406.81, "y": 2477.48, "z": 70.96}, {"x": 5407.18, "y": 2476.95, "z": 70.96}, {"x": 5415.74, "y": 2468.82, "z": 71.24}, {"x": 5429.01, "y": 2456.49, "z": 71.61}, {"x": 5431.16, "y": 2454.5, "z": 71.66}, {"x": 5431.73, "y": 2454.26, "z": 71.68}, {"x": 5432.37, "y": 2454.23, "z": 71.72}, {"x": 5433.28, "y": 2454.67, "z": 71.8}, {"x": 5434.31, "y": 2455.54, "z": 71.89}, {"x": 5435.79, "y": 2458.03, "z": 71.97}, {"x": 5437.26, "y": 2460.85, "z": 72.09}, {"x": 5444.31, "y": 2456.64, "z": 72.31}, {"x": 5442.01, "y": 2453.09, "z": 72.22}, {"x": 5439.64, "y": 2449.36, "z": 72.01}, {"x": 5439.3, "y": 2448.24, "z": 71.95}, {"x": 5439.22, "y": 2447.44, "z": 71.91}, {"x": 5439.37, "y": 2446.89, "z": 71.9}, {"x": 5447.48, "y": 2439.38, "z": 72.17}, {"x": 5460.0, "y": 2427.65, "z": 72.55}, {"x": 5460.0, "y": 2413.0, "z": 72.55}, {"x": 5451.32, "y": 2420.98, "z": 72.28}, {"x": 5438.82, "y": 2432.66, "z": 71.93}, {"x": 5438.44, "y": 2432.89, "z": 71.9}, {"x": 5437.91, "y": 2433.03, "z": 71.89}, {"x": 5437.33, "y": 2433.03, "z": 71.88}, {"x": 5436.8, "y": 2432.81, "z": 71.89}, {"x": 5436.4, "y": 2432.47, "z": 71.9}, {"x": 5435.27, "y": 2430.69, "z": 71.97}, {"x": 5429.91, "y": 2420.0, "z": 72.37}, {"x": 5429.84, "y": 2419.74, "z": 72.37}, {"x": 5429.94, "y": 2419.47, "z": 72.39}, {"x": 5430.33, "y": 2418.97, "z": 72.42}, {"x": 5427.64, "y": 2414.21, "z": 72.45}, {"x": 5427.46, "y": 2413.74, "z": 72.46}, {"x": 5427.51, "y": 2413.25, "z": 72.47}, {"x": 5427.43, "y": 2412.79, "z": 72.48}, {"x": 5417.43, "y": 2393.67, "z": 72.42}, {"x": 5416.4, "y": 2393.79, "z": 72.4}, {"x": 5415.96, "y": 2393.61, "z": 72.4}, {"x": 5415.66, "y": 2393.16, "z": 72.41}, {"x": 5415.68, "y": 2392.65, "z": 72.42}, {"x": 5416.29, "y": 2391.61, "z": 72.42}, {"x": 5405.19, "y": 2370.0, "z": 72.21}, {"x": 5398.68, "y": 2370.0, "z": 72.23}, {"x": 5412.72, "y": 2398.65, "z": 72.31}, {"x": 5412.96, "y": 2398.93, "z": 72.32}, {"x": 5413.35, "y": 2399.27, "z": 72.33}, {"x": 5414.51, "y": 2400.14, "z": 72.39}, {"x": 5414.83, "y": 2400.42, "z": 72.41}, {"x": 5415.01, "y": 2400.68, "z": 72.43}, {"x": 5429.81, "y": 2429.05, "z": 71.98}, {"x": 5433.39, "y": 2435.75, "z": 71.85}, {"x": 5433.57, "y": 2436.33, "z": 71.83}, {"x": 5433.61, "y": 2436.95, "z": 71.81}, {"x": 5433.42, "y": 2437.47, "z": 71.81}, {"x": 5432.99, "y": 2438.16, "z": 71.79}, {"x": 5429.12, "y": 2441.83, "z": 71.7}, {"x": 5416.39, "y": 2453.73, "z": 71.32}, {"x": 5405.1, "y": 2464.17, "z": 71.01}, {"x": 5403.52, "y": 2465.64, "z": 70.99}, {"x": 5402.72, "y": 2466.1, "z": 70.96}, {"x": 5401.92, "y": 2466.32, "z": 70.95}, {"x": 5401.1, "y": 2466.31, "z": 70.92}, {"x": 5400.25, "y": 2466.06, "z": 70.89}, {"x": 5399.54, "y": 2465.57, "z": 70.9}, {"x": 5398.7, "y": 2464.45, "z": 70.94}, {"x": 5397.99, "y": 2463.21, "z": 70.92}, {"x": 5397.92, "y": 2462.5, "z": 70.98}, {"x": 5398.21, "y": 2461.98, "z": 71.05}, {"x": 5400.46, "y": 2460.77, "z": 71.21}, {"x": 5397.76, "y": 2455.23, "z": 71.28}, {"x": 5395.77, "y": 2456.31, "z": 71.12}, {"x": 5395.19, "y": 2456.62, "z": 71.02}, {"x": 5394.69, "y": 2456.63, "z": 70.97}, {"x": 5394.26, "y": 2456.33, "z": 70.94}, {"x": 5393.0, "y": 2454.01, "z": 70.98}, {"x": 5392.42, "y": 2453.12, "z": 70.98}, {"x": 5391.96, "y": 2452.29, "z": 70.96}, {"x": 5385.03, "y": 2438.94, "z": 70.98}, {"x": 5380.0, "y": 2429.16, "z": 71.06}, {"x": 5374.81, "y": 2419.22, "z": 71.14}, {"x": 5370.76, "y": 2411.41, "z": 71.18}, {"x": 5367.13, "y": 2404.45, "z": 71.24}, {"x": 5364.74, "y": 2400.0, "z": 71.28}], "id": 1224493}}} \ No newline at end of file diff --git a/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/sensors/lidar/315966265259836000.feather b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/sensors/lidar/315966265259836000.feather new file mode 100644 index 00000000..f5c2df94 Binary files /dev/null and b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/sensors/lidar/315966265259836000.feather differ diff --git a/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/sensors/lidar/315966265360032000.feather b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/sensors/lidar/315966265360032000.feather new file mode 100644 index 00000000..f950a7ba Binary files /dev/null and b/tests/unit/test_data/sensor/test/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/sensors/lidar/315966265360032000.feather differ diff --git a/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/annotations.feather b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/annotations.feather new file mode 100644 index 00000000..918fe40f Binary files /dev/null and b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/annotations.feather differ diff --git a/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/calibration/egovehicle_SE3_sensor.feather b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/calibration/egovehicle_SE3_sensor.feather new file mode 100644 index 00000000..94096a15 Binary files /dev/null and b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/calibration/egovehicle_SE3_sensor.feather differ diff --git a/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/calibration/intrinsics.feather b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/calibration/intrinsics.feather new file mode 100644 index 00000000..298b7572 Binary files /dev/null and b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/calibration/intrinsics.feather differ diff --git a/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/city_SE3_egovehicle.feather b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/city_SE3_egovehicle.feather new file mode 100644 index 00000000..5695387e Binary files /dev/null and b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/city_SE3_egovehicle.feather differ diff --git a/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/ego_motion.npz b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/ego_motion.npz new file mode 100644 index 00000000..c66c5537 Binary files /dev/null and b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/ego_motion.npz differ diff --git a/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/flow_labels.feather b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/flow_labels.feather new file mode 100644 index 00000000..7df052a0 Binary files /dev/null and b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/flow_labels.feather differ diff --git a/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/7fab2350-7eaf-3b7e-a39d-6937a4c1bede___img_Sim2_city.json b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/7fab2350-7eaf-3b7e-a39d-6937a4c1bede___img_Sim2_city.json new file mode 100644 index 00000000..e29cc7e7 --- /dev/null +++ b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/7fab2350-7eaf-3b7e-a39d-6937a4c1bede___img_Sim2_city.json @@ -0,0 +1 @@ +{"R": [1.0, 0.0, 0.0, 1.0], "t": [-5072.400390625, -2283.900390625], "s": 3.3333333333333335} \ No newline at end of file diff --git a/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/7fab2350-7eaf-3b7e-a39d-6937a4c1bede_ground_height_surface____PIT.npy b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/7fab2350-7eaf-3b7e-a39d-6937a4c1bede_ground_height_surface____PIT.npy new file mode 100644 index 00000000..c166fdfb Binary files /dev/null and b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/7fab2350-7eaf-3b7e-a39d-6937a4c1bede_ground_height_surface____PIT.npy differ diff --git a/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/log_map_archive_7fab2350-7eaf-3b7e-a39d-6937a4c1bede____PIT_city_47896.json b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/log_map_archive_7fab2350-7eaf-3b7e-a39d-6937a4c1bede____PIT_city_47896.json new file mode 100644 index 00000000..c93311c5 --- /dev/null +++ b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/map/log_map_archive_7fab2350-7eaf-3b7e-a39d-6937a4c1bede____PIT_city_47896.json @@ -0,0 +1 @@ +{"pedestrian_crossings": {"2356431": {"edge1": [{"x": 5236.97, "y": 2364.34, "z": 69.5}, {"x": 5232.12, "y": 2367.74, "z": 69.33}], "edge2": [{"x": 5239.78, "y": 2365.57, "z": 69.48}, {"x": 5231.75, "y": 2371.19, "z": 69.24}], "id": 2356431}, "2356430": {"edge1": [{"x": 5231.16, "y": 2371.82, "z": 69.22}, {"x": 5231.28, "y": 2389.37, "z": 68.83}], "edge2": [{"x": 5232.5, "y": 2368.79, "z": 69.31}, {"x": 5234.11, "y": 2389.29, "z": 68.86}], "id": 2356430}, "2356429": {"edge1": [{"x": 5241.69, "y": 2382.09, "z": 69.24}, {"x": 5231.72, "y": 2389.22, "z": 68.84}], "edge2": [{"x": 5241.34, "y": 2386.21, "z": 69.15}, {"x": 5235.41, "y": 2390.58, "z": 68.91}], "id": 2356429}, "2356428": {"edge1": [{"x": 5237.61, "y": 2364.89, "z": 69.46}, {"x": 5240.52, "y": 2384.47, "z": 69.12}], "edge2": [{"x": 5240.88, "y": 2364.97, "z": 69.51}, {"x": 5241.81, "y": 2382.09, "z": 69.25}], "id": 2356428}, "2356005": {"edge1": [{"x": 5158.06, "y": 2443.44, "z": 65.63}, {"x": 5146.42, "y": 2427.11, "z": 65.6}], "edge2": [{"x": 5154.27, "y": 2442.85, "z": 65.53}, {"x": 5145.38, "y": 2430.3, "z": 65.57}], "id": 2356005}, "2356004": {"edge1": [{"x": 5152.93, "y": 2421.62, "z": 65.81}, {"x": 5146.07, "y": 2426.42, "z": 65.6}], "edge2": [{"x": 5156.76, "y": 2422.34, "z": 66.01}, {"x": 5145.53, "y": 2430.29, "z": 65.57}], "id": 2356004}, "2356003": {"edge1": [{"x": 5165.63, "y": 2434.47, "z": 66.03}, {"x": 5156.6, "y": 2422.53, "z": 65.99}], "edge2": [{"x": 5165.03, "y": 2438.24, "z": 65.96}, {"x": 5152.83, "y": 2421.66, "z": 65.79}], "id": 2356003}, "2356002": {"edge1": [{"x": 5165.18, "y": 2438.24, "z": 65.97}, {"x": 5158.76, "y": 2443.99, "z": 65.67}], "edge2": [{"x": 5165.8, "y": 2434.43, "z": 66.04}, {"x": 5154.46, "y": 2442.76, "z": 65.53}], "id": 2356002}, "2355546": {"edge1": [{"x": 5319.6, "y": 2331.85, "z": 71.88}, {"x": 5314.36, "y": 2314.22, "z": 72.09}], "edge2": [{"x": 5316.65, "y": 2332.14, "z": 71.89}, {"x": 5311.81, "y": 2315.6, "z": 72.02}], "id": 2355546}, "2355541": {"edge1": [{"x": 5316.13, "y": 2331.42, "z": 71.86}, {"x": 5327.01, "y": 2323.93, "z": 72.02}], "edge2": [{"x": 5319.37, "y": 2333.04, "z": 71.97}, {"x": 5327.8, "y": 2327.46, "z": 72.07}], "id": 2355541}, "2356225": {"edge1": [{"x": 5079.03, "y": 2471.46, "z": 62.84}, {"x": 5090.62, "y": 2463.72, "z": 63.2}], "edge2": [{"x": 5082.22, "y": 2472.42, "z": 62.87}, {"x": 5096.85, "y": 2462.84, "z": 63.44}], "id": 2356225}}, "lane_segments": {"38109167": {"id": 38109167, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5272.94, "y": 2353.69, "z": 70.51}, {"x": 5286.78, "y": 2342.58, "z": 71.04}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5268.73, "y": 2346.16, "z": 70.46}, {"x": 5285.11, "y": 2340.16, "z": 71.03}], "right_lane_mark_type": "NONE", "successors": [38109400], "predecessors": [38117100], "right_neighbor_id": null, "left_neighbor_id": 38109519}, "38109176": {"id": 38109176, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5343.55, "y": 2359.71, "z": 71.69}, {"x": 5327.53, "y": 2329.17, "z": 72.01}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5336.53, "y": 2363.4, "z": 71.7}, {"x": 5323.36, "y": 2338.28, "z": 71.9}, {"x": 5320.76, "y": 2333.27, "z": 71.87}], "right_lane_mark_type": "NONE", "successors": [38111935, 38109260, 38109748, 38109698, 38111936], "predecessors": [38109432, 38109421], "right_neighbor_id": null, "left_neighbor_id": null}, "38109234": {"id": 38109234, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5304.84, "y": 2330.0, "z": 71.66}, {"x": 5286.78, "y": 2342.58, "z": 71.04}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5306.47, "y": 2332.86, "z": 71.62}, {"x": 5288.86, "y": 2345.49, "z": 70.99}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38109519, 38111601], "predecessors": [38111133], "right_neighbor_id": 38111904, "left_neighbor_id": 38109400}, "38109262": {"id": 38109262, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5280.0, "y": 2413.56, "z": 70.11}, {"x": 5296.66, "y": 2402.37, "z": 70.48}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5280.0, "y": 2407.51, "z": 70.05}, {"x": 5290.94, "y": 2400.19, "z": 70.32}, {"x": 5293.79, "y": 2398.19, "z": 70.4}], "right_lane_mark_type": "NONE", "successors": [38116557, 38116487], "predecessors": [38114654], "right_neighbor_id": null, "left_neighbor_id": null}, "38109290": {"id": 38109290, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5310.26, "y": 2326.29, "z": 71.83}, {"x": 5330.21, "y": 2312.7, "z": 72.05}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5308.45, "y": 2323.82, "z": 71.8}, {"x": 5328.5, "y": 2310.17, "z": 72.07}], "right_lane_mark_type": "NONE", "successors": [38109324], "predecessors": [38111103], "right_neighbor_id": 38111849, "left_neighbor_id": 38109397}, "38109317": {"id": 38109317, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5310.26, "y": 2326.29, "z": 71.83}, {"x": 5311.05, "y": 2325.8, "z": 71.85}, {"x": 5311.86, "y": 2325.3, "z": 71.88}, {"x": 5312.67, "y": 2324.78, "z": 71.9}, {"x": 5313.49, "y": 2324.24, "z": 71.92}, {"x": 5314.31, "y": 2323.69, "z": 71.94}, {"x": 5315.13, "y": 2323.13, "z": 71.95}, {"x": 5315.93, "y": 2322.54, "z": 71.97}, {"x": 5316.72, "y": 2321.94, "z": 71.98}, {"x": 5317.49, "y": 2321.32, "z": 71.99}, {"x": 5318.23, "y": 2320.69, "z": 72.01}, {"x": 5318.93, "y": 2320.03, "z": 72.02}, {"x": 5319.6, "y": 2319.36, "z": 72.02}, {"x": 5320.22, "y": 2318.66, "z": 72.02}, {"x": 5320.8, "y": 2317.94, "z": 72.03}, {"x": 5321.32, "y": 2317.21, "z": 72.03}, {"x": 5321.78, "y": 2316.45, "z": 72.04}, {"x": 5322.18, "y": 2315.67, "z": 72.05}, {"x": 5322.5, "y": 2314.87, "z": 72.05}, {"x": 5322.75, "y": 2314.05, "z": 72.06}, {"x": 5322.92, "y": 2313.2, "z": 72.08}, {"x": 5323.0, "y": 2312.33, "z": 72.1}, {"x": 5322.99, "y": 2311.43, "z": 72.12}, {"x": 5322.88, "y": 2310.52, "z": 72.13}, {"x": 5322.67, "y": 2309.57, "z": 72.14}, {"x": 5322.35, "y": 2308.6, "z": 72.15}, {"x": 5321.92, "y": 2307.61, "z": 72.19}, {"x": 5321.42, "y": 2306.55, "z": 72.28}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5308.45, "y": 2323.82, "z": 71.8}, {"x": 5309.21, "y": 2323.24, "z": 71.82}, {"x": 5309.84, "y": 2322.68, "z": 71.84}, {"x": 5310.59, "y": 2322.1, "z": 71.86}, {"x": 5311.37, "y": 2321.46, "z": 71.88}, {"x": 5312.18, "y": 2320.78, "z": 71.9}, {"x": 5312.99, "y": 2320.06, "z": 71.93}, {"x": 5313.78, "y": 2319.3, "z": 71.95}, {"x": 5314.54, "y": 2318.52, "z": 71.97}, {"x": 5315.23, "y": 2317.71, "z": 71.99}, {"x": 5315.85, "y": 2316.9, "z": 72.0}, {"x": 5316.36, "y": 2316.08, "z": 72.02}, {"x": 5316.76, "y": 2315.26, "z": 72.03}, {"x": 5317.01, "y": 2314.45, "z": 72.04}, {"x": 5317.1, "y": 2313.65, "z": 72.05}, {"x": 5317.01, "y": 2312.88, "z": 72.06}, {"x": 5316.72, "y": 2312.13, "z": 72.11}, {"x": 5316.2, "y": 2311.43, "z": 72.15}, {"x": 5315.57, "y": 2310.62, "z": 72.21}], "right_lane_mark_type": "NONE", "successors": [38109302], "predecessors": [38111103], "right_neighbor_id": null, "left_neighbor_id": null}, "38109359": {"id": 38109359, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5244.11, "y": 2370.36, "z": 69.6}, {"x": 5264.5, "y": 2359.3, "z": 70.24}, {"x": 5264.72, "y": 2359.16, "z": 70.25}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5240.78, "y": 2365.26, "z": 69.5}, {"x": 5250.55, "y": 2358.63, "z": 69.81}, {"x": 5260.02, "y": 2352.14, "z": 70.15}], "right_lane_mark_type": "NONE", "successors": [38117100], "predecessors": [38114446, 38114374], "right_neighbor_id": null, "left_neighbor_id": null}, "38109382": {"id": 38109382, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5272.94, "y": 2353.69, "z": 70.51}, {"x": 5264.72, "y": 2359.16, "z": 70.25}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5275.94, "y": 2358.54, "z": 70.49}, {"x": 5268.05, "y": 2363.91, "z": 70.19}], "right_lane_mark_type": "NONE", "successors": [38116085], "predecessors": [38111894, 38109519], "right_neighbor_id": null, "left_neighbor_id": 38117100}, "38109397": {"id": 38109397, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5330.21, "y": 2312.7, "z": 72.05}, {"x": 5310.26, "y": 2326.29, "z": 71.83}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5332.07, "y": 2315.53, "z": 72.0}, {"x": 5312.04, "y": 2329.04, "z": 71.8}], "right_lane_mark_type": "NONE", "successors": [38111133], "predecessors": [38109434], "right_neighbor_id": 38111902, "left_neighbor_id": 38109290}, "38109400": {"id": 38109400, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5286.78, "y": 2342.58, "z": 71.04}, {"x": 5304.84, "y": 2330.0, "z": 71.66}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5285.11, "y": 2340.16, "z": 71.03}, {"x": 5303.12, "y": 2327.48, "z": 71.65}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111103], "predecessors": [38109167], "right_neighbor_id": 38111866, "left_neighbor_id": 38109234}, "38109440": {"id": 38109440, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5272.94, "y": 2353.69, "z": 70.51}, {"x": 5275.78, "y": 2351.57, "z": 70.61}, {"x": 5276.51, "y": 2350.94, "z": 70.64}, {"x": 5277.22, "y": 2350.28, "z": 70.67}, {"x": 5277.92, "y": 2349.58, "z": 70.7}, {"x": 5278.59, "y": 2348.85, "z": 70.73}, {"x": 5279.21, "y": 2348.1, "z": 70.76}, {"x": 5279.78, "y": 2347.32, "z": 70.79}, {"x": 5280.29, "y": 2346.52, "z": 70.82}, {"x": 5280.73, "y": 2345.71, "z": 70.84}, {"x": 5281.09, "y": 2344.88, "z": 70.86}, {"x": 5281.36, "y": 2344.03, "z": 70.88}, {"x": 5281.53, "y": 2343.18, "z": 70.9}, {"x": 5281.58, "y": 2342.32, "z": 70.91}, {"x": 5281.51, "y": 2341.45, "z": 70.92}, {"x": 5281.32, "y": 2340.59, "z": 70.92}, {"x": 5280.98, "y": 2339.72, "z": 70.91}, {"x": 5280.49, "y": 2338.86, "z": 70.88}, {"x": 5279.45, "y": 2337.18, "z": 70.9}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5268.73, "y": 2346.16, "z": 70.46}, {"x": 5268.88, "y": 2346.0, "z": 70.46}, {"x": 5269.41, "y": 2345.65, "z": 70.49}, {"x": 5274.23, "y": 2342.45, "z": 70.67}, {"x": 5274.38, "y": 2342.19, "z": 70.68}, {"x": 5274.61, "y": 2341.9, "z": 70.69}, {"x": 5274.82, "y": 2341.43, "z": 70.68}, {"x": 5274.96, "y": 2341.19, "z": 70.68}, {"x": 5275.04, "y": 2340.73, "z": 70.69}, {"x": 5275.01, "y": 2340.32, "z": 70.71}], "right_lane_mark_type": "NONE", "successors": [38109482], "predecessors": [38117100], "right_neighbor_id": null, "left_neighbor_id": null}, "38109482": {"id": 38109482, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5279.45, "y": 2337.18, "z": 70.9}, {"x": 5279.21, "y": 2336.78, "z": 70.9}, {"x": 5278.95, "y": 2336.41, "z": 70.9}, {"x": 5274.25, "y": 2328.25, "z": 71.24}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5275.01, "y": 2340.32, "z": 70.71}, {"x": 5272.96, "y": 2337.26, "z": 70.88}, {"x": 5269.43, "y": 2331.55, "z": 71.14}], "right_lane_mark_type": "NONE", "successors": [38115599], "predecessors": [38109440, 38111601, 38111937], "right_neighbor_id": null, "left_neighbor_id": null}, "38109519": {"id": 38109519, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5286.78, "y": 2342.58, "z": 71.04}, {"x": 5272.94, "y": 2353.69, "z": 70.51}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5288.86, "y": 2345.49, "z": 70.99}, {"x": 5275.94, "y": 2358.54, "z": 70.49}], "right_lane_mark_type": "NONE", "successors": [38109382], "predecessors": [38109234], "right_neighbor_id": null, "left_neighbor_id": 38109167}, "38109698": {"id": 38109698, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5327.53, "y": 2329.17, "z": 72.01}, {"x": 5326.4, "y": 2327.28, "z": 72.0}, {"x": 5325.75, "y": 2326.62, "z": 71.96}, {"x": 5325.08, "y": 2326.02, "z": 71.94}, {"x": 5324.39, "y": 2325.46, "z": 71.96}, {"x": 5323.68, "y": 2324.97, "z": 71.97}, {"x": 5322.94, "y": 2324.52, "z": 71.99}, {"x": 5322.18, "y": 2324.14, "z": 72.0}, {"x": 5321.4, "y": 2323.82, "z": 72.0}, {"x": 5320.61, "y": 2323.56, "z": 72.0}, {"x": 5319.8, "y": 2323.37, "z": 72.0}, {"x": 5318.98, "y": 2323.24, "z": 72.0}, {"x": 5318.14, "y": 2323.19, "z": 71.99}, {"x": 5317.29, "y": 2323.21, "z": 71.98}, {"x": 5316.43, "y": 2323.31, "z": 71.97}, {"x": 5315.57, "y": 2323.48, "z": 71.96}, {"x": 5314.69, "y": 2323.73, "z": 71.94}, {"x": 5313.81, "y": 2324.07, "z": 71.93}, {"x": 5312.93, "y": 2324.49, "z": 71.91}, {"x": 5312.09, "y": 2325.02, "z": 71.89}, {"x": 5311.19, "y": 2325.66, "z": 71.86}, {"x": 5310.26, "y": 2326.29, "z": 71.83}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5320.76, "y": 2333.27, "z": 71.87}, {"x": 5319.93, "y": 2331.63, "z": 71.85}, {"x": 5319.51, "y": 2330.85, "z": 71.84}, {"x": 5319.0, "y": 2330.16, "z": 71.83}, {"x": 5318.4, "y": 2329.56, "z": 71.84}, {"x": 5317.73, "y": 2329.07, "z": 71.86}, {"x": 5316.85, "y": 2328.68, "z": 71.88}, {"x": 5316.01, "y": 2328.54, "z": 71.87}, {"x": 5315.24, "y": 2328.53, "z": 71.87}, {"x": 5314.22, "y": 2328.58, "z": 71.85}, {"x": 5313.34, "y": 2328.68, "z": 71.83}, {"x": 5312.53, "y": 2328.88, "z": 71.81}, {"x": 5312.04, "y": 2329.04, "z": 71.8}], "right_lane_mark_type": "NONE", "successors": [38111133], "predecessors": [38109176], "right_neighbor_id": null, "left_neighbor_id": null}, "38109824": {"id": 38109824, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5244.23, "y": 2400.0, "z": 69.18}, {"x": 5247.77, "y": 2404.68, "z": 69.27}, {"x": 5257.14, "y": 2419.46, "z": 69.48}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5251.14, "y": 2400.0, "z": 69.24}, {"x": 5262.48, "y": 2415.83, "z": 69.51}], "right_lane_mark_type": "NONE", "successors": [38114712, 38114672], "predecessors": [38114332], "right_neighbor_id": null, "left_neighbor_id": null}, "38110982": {"id": 38110982, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5205.75, "y": 2399.69, "z": 67.93}, {"x": 5185.87, "y": 2413.3, "z": 67.05}, {"x": 5180.46, "y": 2416.73, "z": 66.82}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5209.27, "y": 2404.3, "z": 67.86}, {"x": 5193.6, "y": 2415.04, "z": 67.19}, {"x": 5184.25, "y": 2421.43, "z": 66.78}], "right_lane_mark_type": "NONE", "successors": [38111662], "predecessors": [38114432], "right_neighbor_id": null, "left_neighbor_id": 38133156}, "38110983": {"id": 38110983, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5135.52, "y": 2449.51, "z": 64.99}, {"x": 5130.0, "y": 2453.5, "z": 64.81}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5137.92, "y": 2452.76, "z": 64.93}, {"x": 5130.0, "y": 2458.55, "z": 64.7}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111258], "predecessors": [38111696], "right_neighbor_id": 38111058, "left_neighbor_id": 38110984}, "38110984": {"id": 38110984, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5133.72, "y": 2446.99, "z": 64.98}, {"x": 5130.0, "y": 2449.64, "z": 64.84}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5135.52, "y": 2449.51, "z": 64.99}, {"x": 5130.0, "y": 2453.5, "z": 64.81}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111598], "predecessors": [38111213], "right_neighbor_id": 38110983, "left_neighbor_id": null}, "38110986": {"id": 38110986, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5140.87, "y": 2441.56, "z": 65.24}, {"x": 5147.02, "y": 2437.33, "z": 65.49}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5139.07, "y": 2438.93, "z": 65.22}, {"x": 5145.22, "y": 2434.75, "z": 65.49}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111243], "predecessors": [38111695], "right_neighbor_id": 38111020, "left_neighbor_id": null}, "38111000": {"id": 38111000, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5113.51, "y": 2473.63, "z": 64.13}, {"x": 5110.03, "y": 2476.77, "z": 63.99}, {"x": 5101.3, "y": 2485.1, "z": 63.73}, {"x": 5095.12, "y": 2491.1, "z": 63.43}, {"x": 5090.57, "y": 2495.35, "z": 63.27}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5116.24, "y": 2475.72, "z": 64.08}, {"x": 5112.65, "y": 2479.28, "z": 63.95}, {"x": 5105.8, "y": 2485.73, "z": 63.68}, {"x": 5093.09, "y": 2497.8, "z": 63.15}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111526], "predecessors": [38111648], "right_neighbor_id": null, "left_neighbor_id": null}, "38111004": {"id": 38111004, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5130.0, "y": 2458.55, "z": 64.7}, {"x": 5124.75, "y": 2462.33, "z": 64.55}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5130.0, "y": 2460.45, "z": 64.65}, {"x": 5125.66, "y": 2463.59, "z": 64.52}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111825, 38111786], "predecessors": [38111058], "right_neighbor_id": null, "left_neighbor_id": 38111258}, "38111020": {"id": 38111020, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5139.07, "y": 2438.93, "z": 65.22}, {"x": 5145.22, "y": 2434.75, "z": 65.49}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5137.14, "y": 2436.07, "z": 65.21}, {"x": 5143.22, "y": 2431.87, "z": 65.46}], "right_lane_mark_type": "NONE", "successors": [38111512], "predecessors": [38111212], "right_neighbor_id": null, "left_neighbor_id": 38110986}, "38111058": {"id": 38111058, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5137.92, "y": 2452.76, "z": 64.93}, {"x": 5130.0, "y": 2458.55, "z": 64.7}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5138.82, "y": 2453.97, "z": 64.91}, {"x": 5130.0, "y": 2460.45, "z": 64.65}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111004], "predecessors": [38111126], "right_neighbor_id": null, "left_neighbor_id": 38110983}, "38111090": {"id": 38111090, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5130.0, "y": 2449.04, "z": 64.84}, {"x": 5133.55, "y": 2446.6, "z": 64.97}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5130.0, "y": 2445.16, "z": 64.86}, {"x": 5131.72, "y": 2443.93, "z": 64.94}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111695], "predecessors": [38111649], "right_neighbor_id": 38111405, "left_neighbor_id": null}, "38111103": {"id": 38111103, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5304.84, "y": 2330.0, "z": 71.66}, {"x": 5310.26, "y": 2326.29, "z": 71.83}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5303.12, "y": 2327.48, "z": 71.65}, {"x": 5308.45, "y": 2323.82, "z": 71.8}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38109317, 38109290], "predecessors": [38109400], "right_neighbor_id": 38111861, "left_neighbor_id": 38111133}, "38111117": {"id": 38111117, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5111.95, "y": 2461.87, "z": 64.1}, {"x": 5101.54, "y": 2468.99, "z": 63.67}, {"x": 5096.29, "y": 2472.64, "z": 63.44}, {"x": 5092.62, "y": 2475.23, "z": 63.29}, {"x": 5090.22, "y": 2476.84, "z": 63.2}, {"x": 5088.24, "y": 2478.13, "z": 63.11}, {"x": 5087.42, "y": 2478.65, "z": 63.08}, {"x": 5086.51, "y": 2479.21, "z": 63.05}, {"x": 5085.43, "y": 2479.83, "z": 63.01}, {"x": 5084.47, "y": 2480.36, "z": 62.98}, {"x": 5083.22, "y": 2480.91, "z": 62.92}, {"x": 5081.58, "y": 2481.66, "z": 62.86}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5113.84, "y": 2464.62, "z": 64.19}, {"x": 5108.05, "y": 2468.59, "z": 63.95}, {"x": 5102.35, "y": 2472.56, "z": 63.72}, {"x": 5096.35, "y": 2476.77, "z": 63.47}, {"x": 5094.43, "y": 2478.06, "z": 63.39}, {"x": 5091.61, "y": 2479.89, "z": 63.27}, {"x": 5089.72, "y": 2481.04, "z": 63.2}, {"x": 5088.43, "y": 2481.86, "z": 63.14}, {"x": 5087.28, "y": 2482.56, "z": 63.09}, {"x": 5086.34, "y": 2483.09, "z": 63.05}, {"x": 5085.14, "y": 2483.71, "z": 63.01}, {"x": 5084.23, "y": 2484.17, "z": 62.98}, {"x": 5083.14, "y": 2484.66, "z": 62.94}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111164], "predecessors": [38111770], "right_neighbor_id": 38111615, "left_neighbor_id": null}, "38111126": {"id": 38111126, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5145.11, "y": 2447.42, "z": 65.17}, {"x": 5137.92, "y": 2452.76, "z": 64.93}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5145.97, "y": 2448.56, "z": 65.22}, {"x": 5138.82, "y": 2453.97, "z": 64.91}], "right_lane_mark_type": "NONE", "successors": [38111058], "predecessors": [38111629], "right_neighbor_id": null, "left_neighbor_id": 38111696}, "38111133": {"id": 38111133, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5310.26, "y": 2326.29, "z": 71.83}, {"x": 5304.84, "y": 2330.0, "z": 71.66}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5312.04, "y": 2329.04, "z": 71.8}, {"x": 5306.47, "y": 2332.86, "z": 71.62}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38109234], "predecessors": [38109698, 38109397], "right_neighbor_id": 38111905, "left_neighbor_id": 38111103}, "38111163": {"id": 38111163, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5130.0, "y": 2456.9, "z": 64.75}, {"x": 5124.76, "y": 2462.32, "z": 64.55}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5130.0, "y": 2462.47, "z": 64.59}, {"x": 5126.93, "y": 2465.41, "z": 64.47}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111228], "predecessors": [38114770], "right_neighbor_id": null, "left_neighbor_id": null}, "38111173": {"id": 38111173, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5148.71, "y": 2439.81, "z": 65.47}, {"x": 5142.73, "y": 2444.2, "z": 65.25}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5150.88, "y": 2443.13, "z": 65.39}, {"x": 5145.11, "y": 2447.42, "z": 65.17}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111696], "predecessors": [38111884, 38111446], "right_neighbor_id": 38111629, "left_neighbor_id": 38111540}, "38111175": {"id": 38111175, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.03, "y": 2426.94, "z": 66.29}, {"x": 5147.27, "y": 2437.68, "z": 65.49}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5169.46, "y": 2431.57, "z": 66.22}, {"x": 5148.71, "y": 2439.81, "z": 65.47}], "right_lane_mark_type": "NONE", "successors": [38111540], "predecessors": [38111662], "right_neighbor_id": null, "left_neighbor_id": null}, "38111212": {"id": 38111212, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5131.72, "y": 2443.93, "z": 64.94}, {"x": 5139.07, "y": 2438.93, "z": 65.22}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5131.33, "y": 2440.05, "z": 64.91}, {"x": 5137.14, "y": 2436.07, "z": 65.21}], "right_lane_mark_type": "NONE", "successors": [38111020], "predecessors": [38111405], "right_neighbor_id": null, "left_neighbor_id": 38111695}, "38111213": {"id": 38111213, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5141.1, "y": 2441.92, "z": 65.25}, {"x": 5133.72, "y": 2446.99, "z": 64.98}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5142.73, "y": 2444.2, "z": 65.25}, {"x": 5135.52, "y": 2449.51, "z": 64.99}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38110984], "predecessors": [38111540], "right_neighbor_id": 38111696, "left_neighbor_id": null}, "38111228": {"id": 38111228, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5124.76, "y": 2462.32, "z": 64.55}, {"x": 5117.46, "y": 2469.61, "z": 64.25}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5126.93, "y": 2465.41, "z": 64.47}, {"x": 5119.56, "y": 2472.48, "z": 64.2}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111648], "predecessors": [38111163], "right_neighbor_id": null, "left_neighbor_id": null}, "38111243": {"id": 38111243, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5147.02, "y": 2437.33, "z": 65.49}, {"x": 5164.69, "y": 2425.75, "z": 66.29}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5145.22, "y": 2434.75, "z": 65.49}, {"x": 5160.51, "y": 2419.95, "z": 66.15}], "right_lane_mark_type": "NONE", "successors": [38133154, 38133155], "predecessors": [38110986], "right_neighbor_id": null, "left_neighbor_id": null}, "38111258": {"id": 38111258, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5130.0, "y": 2453.5, "z": 64.81}, {"x": 5122.33, "y": 2458.78, "z": 64.51}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5130.0, "y": 2458.55, "z": 64.7}, {"x": 5124.75, "y": 2462.33, "z": 64.55}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111737], "predecessors": [38110983], "right_neighbor_id": 38111004, "left_neighbor_id": 38111598}, "38111259": {"id": 38111259, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5079.73, "y": 2477.78, "z": 62.79}, {"x": 5081.13, "y": 2477.18, "z": 62.83}, {"x": 5083.03, "y": 2476.35, "z": 62.91}, {"x": 5085.05, "y": 2475.33, "z": 62.98}, {"x": 5087.4, "y": 2474.04, "z": 63.07}, {"x": 5092.55, "y": 2470.61, "z": 63.28}, {"x": 5100.04, "y": 2465.55, "z": 63.61}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5078.14, "y": 2474.28, "z": 62.74}, {"x": 5083.13, "y": 2472.18, "z": 62.87}, {"x": 5084.01, "y": 2471.7, "z": 62.9}, {"x": 5086.97, "y": 2469.92, "z": 63.0}, {"x": 5091.69, "y": 2466.83, "z": 63.17}, {"x": 5095.67, "y": 2464.08, "z": 63.37}, {"x": 5097.86, "y": 2462.61, "z": 63.48}], "right_lane_mark_type": "NONE", "successors": [38111774], "predecessors": [38111397], "right_neighbor_id": null, "left_neighbor_id": 38111425}, "38111278": {"id": 38111278, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5166.03, "y": 2426.94, "z": 66.29}, {"x": 5150.88, "y": 2443.13, "z": 65.39}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5169.46, "y": 2431.57, "z": 66.22}, {"x": 5166.8, "y": 2433.46, "z": 66.08}, {"x": 5166.46, "y": 2433.75, "z": 66.08}, {"x": 5151.76, "y": 2444.37, "z": 65.42}], "right_lane_mark_type": "NONE", "successors": [38111629], "predecessors": [38111662], "right_neighbor_id": null, "left_neighbor_id": null}, "38111310": {"id": 38111310, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.37, "y": 2440.79, "z": 65.95}, {"x": 5152.69, "y": 2421.54, "z": 65.79}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5160.14, "y": 2445.81, "z": 65.74}, {"x": 5146.08, "y": 2426.08, "z": 65.61}], "right_lane_mark_type": "NONE", "successors": [38111342], "predecessors": [38119951], "right_neighbor_id": null, "left_neighbor_id": null}, "38111330": {"id": 38111330, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5116.4, "y": 2468.26, "z": 64.25}, {"x": 5111.95, "y": 2471.45, "z": 64.09}, {"x": 5109.72, "y": 2473.07, "z": 64.02}, {"x": 5107.1, "y": 2474.87, "z": 63.9}, {"x": 5103.61, "y": 2477.16, "z": 63.76}, {"x": 5099.76, "y": 2479.78, "z": 63.6}, {"x": 5095.35, "y": 2482.63, "z": 63.41}, {"x": 5091.99, "y": 2484.45, "z": 63.28}, {"x": 5088.37, "y": 2486.26, "z": 63.15}, {"x": 5084.78, "y": 2487.92, "z": 63.01}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5117.29, "y": 2469.51, "z": 64.25}, {"x": 5115.69, "y": 2470.61, "z": 64.2}, {"x": 5113.0, "y": 2472.51, "z": 64.17}, {"x": 5112.11, "y": 2473.06, "z": 64.17}, {"x": 5107.23, "y": 2476.31, "z": 63.98}, {"x": 5104.65, "y": 2478.19, "z": 63.96}, {"x": 5100.95, "y": 2480.67, "z": 63.77}, {"x": 5098.57, "y": 2482.18, "z": 63.67}, {"x": 5095.81, "y": 2484.07, "z": 63.53}, {"x": 5090.71, "y": 2486.92, "z": 63.34}, {"x": 5085.49, "y": 2489.18, "z": 63.08}], "right_lane_mark_type": "NONE", "successors": [38111783, 38111430], "predecessors": [38111786], "right_neighbor_id": null, "left_neighbor_id": 38111615}, "38111342": {"id": 38111342, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5152.69, "y": 2421.54, "z": 65.79}, {"x": 5151.57, "y": 2420.03, "z": 65.75}, {"x": 5137.17, "y": 2396.17, "z": 65.11}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5146.08, "y": 2426.08, "z": 65.61}, {"x": 5145.96, "y": 2425.82, "z": 65.61}, {"x": 5142.05, "y": 2419.45, "z": 65.43}, {"x": 5130.4, "y": 2400.29, "z": 64.88}], "right_lane_mark_type": "NONE", "successors": [38118957, 38119599], "predecessors": [38111512, 38111880, 38111310], "right_neighbor_id": null, "left_neighbor_id": null}, "38111376": {"id": 38111376, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5201.69, "y": 2490.0, "z": 66.87}, {"x": 5192.71, "y": 2477.54, "z": 66.59}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5191.84, "y": 2490.0, "z": 66.62}, {"x": 5186.24, "y": 2482.2, "z": 66.46}], "right_lane_mark_type": "NONE", "successors": [38119952, 38119951], "predecessors": [38117242], "right_neighbor_id": null, "left_neighbor_id": null}, "38111405": {"id": 38111405, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5130.0, "y": 2445.16, "z": 64.86}, {"x": 5131.72, "y": 2443.93, "z": 64.94}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5130.0, "y": 2440.97, "z": 64.85}, {"x": 5131.33, "y": 2440.05, "z": 64.91}], "right_lane_mark_type": "NONE", "successors": [38111212], "predecessors": [38111604], "right_neighbor_id": null, "left_neighbor_id": 38111090}, "38111425": {"id": 38111425, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5081.26, "y": 2481.11, "z": 62.84}, {"x": 5082.05, "y": 2480.79, "z": 62.88}, {"x": 5082.85, "y": 2480.47, "z": 62.91}, {"x": 5084.73, "y": 2479.52, "z": 62.99}, {"x": 5086.17, "y": 2478.74, "z": 63.03}, {"x": 5087.7, "y": 2477.81, "z": 63.09}, {"x": 5089.18, "y": 2476.86, "z": 63.15}, {"x": 5092.88, "y": 2474.36, "z": 63.3}, {"x": 5101.82, "y": 2468.17, "z": 63.68}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5079.73, "y": 2477.78, "z": 62.79}, {"x": 5081.13, "y": 2477.18, "z": 62.83}, {"x": 5083.03, "y": 2476.35, "z": 62.91}, {"x": 5085.05, "y": 2475.33, "z": 62.98}, {"x": 5087.4, "y": 2474.04, "z": 63.07}, {"x": 5092.55, "y": 2470.61, "z": 63.28}, {"x": 5100.04, "y": 2465.55, "z": 63.61}], "right_lane_mark_type": "NONE", "successors": [38111781], "predecessors": [38111197, 38111398], "right_neighbor_id": 38111259, "left_neighbor_id": null}, "38111446": {"id": 38111446, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.03, "y": 2426.94, "z": 66.29}, {"x": 5148.71, "y": 2439.81, "z": 65.47}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5169.46, "y": 2431.57, "z": 66.22}, {"x": 5150.88, "y": 2443.13, "z": 65.39}], "right_lane_mark_type": "NONE", "successors": [38111173], "predecessors": [38111662], "right_neighbor_id": null, "left_neighbor_id": null}, "38111512": {"id": 38111512, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5145.22, "y": 2434.75, "z": 65.49}, {"x": 5145.96, "y": 2434.28, "z": 65.52}, {"x": 5146.76, "y": 2433.8, "z": 65.56}, {"x": 5147.61, "y": 2433.29, "z": 65.59}, {"x": 5148.43, "y": 2432.77, "z": 65.63}, {"x": 5149.21, "y": 2432.22, "z": 65.66}, {"x": 5149.95, "y": 2431.61, "z": 65.7}, {"x": 5150.65, "y": 2430.97, "z": 65.74}, {"x": 5151.3, "y": 2430.3, "z": 65.76}, {"x": 5151.9, "y": 2429.6, "z": 65.79}, {"x": 5152.43, "y": 2428.88, "z": 65.81}, {"x": 5152.88, "y": 2428.14, "z": 65.83}, {"x": 5153.25, "y": 2427.37, "z": 65.84}, {"x": 5153.52, "y": 2426.58, "z": 65.85}, {"x": 5153.7, "y": 2425.78, "z": 65.85}, {"x": 5153.76, "y": 2424.95, "z": 65.86}, {"x": 5153.69, "y": 2424.12, "z": 65.84}, {"x": 5153.5, "y": 2423.27, "z": 65.81}, {"x": 5153.17, "y": 2422.41, "z": 65.79}, {"x": 5152.69, "y": 2421.54, "z": 65.79}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5143.22, "y": 2431.87, "z": 65.46}, {"x": 5144.12, "y": 2431.28, "z": 65.51}, {"x": 5144.97, "y": 2430.71, "z": 65.53}, {"x": 5145.59, "y": 2430.3, "z": 65.57}, {"x": 5145.67, "y": 2430.2, "z": 65.57}, {"x": 5146.17, "y": 2429.6, "z": 65.57}, {"x": 5146.53, "y": 2428.81, "z": 65.58}, {"x": 5146.59, "y": 2427.88, "z": 65.59}, {"x": 5146.52, "y": 2427.28, "z": 65.6}, {"x": 5146.27, "y": 2426.51, "z": 65.6}, {"x": 5146.08, "y": 2426.08, "z": 65.61}], "right_lane_mark_type": "NONE", "successors": [38111342], "predecessors": [38111020], "right_neighbor_id": null, "left_neighbor_id": null}, "38111540": {"id": 38111540, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5147.27, "y": 2437.68, "z": 65.49}, {"x": 5141.1, "y": 2441.92, "z": 65.25}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5148.71, "y": 2439.81, "z": 65.47}, {"x": 5142.73, "y": 2444.2, "z": 65.25}], "right_lane_mark_type": "NONE", "successors": [38111213], "predecessors": [38111175], "right_neighbor_id": 38111173, "left_neighbor_id": null}, "38111569": {"id": 38111569, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5066.52, "y": 2466.66, "z": 62.14}, {"x": 5068.47, "y": 2467.59, "z": 62.23}, {"x": 5070.87, "y": 2468.57, "z": 62.34}, {"x": 5072.29, "y": 2469.01, "z": 62.4}, {"x": 5073.81, "y": 2469.29, "z": 62.46}, {"x": 5076.67, "y": 2469.75, "z": 62.54}, {"x": 5079.26, "y": 2470.07, "z": 62.65}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5069.85, "y": 2462.07, "z": 62.04}, {"x": 5071.48, "y": 2462.83, "z": 62.13}, {"x": 5073.5, "y": 2463.69, "z": 62.24}, {"x": 5075.66, "y": 2464.42, "z": 62.35}, {"x": 5077.95, "y": 2464.95, "z": 62.47}, {"x": 5080.57, "y": 2465.35, "z": 62.6}, {"x": 5085.44, "y": 2465.44, "z": 62.84}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111751], "predecessors": [38111571], "right_neighbor_id": null, "left_neighbor_id": null}, "38111598": {"id": 38111598, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5130.0, "y": 2449.64, "z": 64.84}, {"x": 5120.49, "y": 2456.1, "z": 64.43}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5130.0, "y": 2453.5, "z": 64.81}, {"x": 5122.33, "y": 2458.78, "z": 64.51}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111770], "predecessors": [38110984], "right_neighbor_id": 38111258, "left_neighbor_id": null}, "38111601": {"id": 38111601, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5286.78, "y": 2342.58, "z": 71.04}, {"x": 5285.92, "y": 2342.62, "z": 71.02}, {"x": 5285.09, "y": 2342.48, "z": 71.01}, {"x": 5284.29, "y": 2342.18, "z": 70.99}, {"x": 5283.52, "y": 2341.75, "z": 70.97}, {"x": 5282.79, "y": 2341.21, "z": 70.96}, {"x": 5282.1, "y": 2340.58, "z": 70.94}, {"x": 5281.46, "y": 2339.91, "z": 70.93}, {"x": 5280.87, "y": 2339.2, "z": 70.91}, {"x": 5280.33, "y": 2338.49, "z": 70.88}, {"x": 5279.86, "y": 2337.81, "z": 70.87}, {"x": 5279.45, "y": 2337.18, "z": 70.9}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5288.86, "y": 2345.49, "z": 70.99}, {"x": 5287.83, "y": 2346.05, "z": 70.96}, {"x": 5287.28, "y": 2346.33, "z": 70.94}, {"x": 5286.71, "y": 2346.55, "z": 70.93}, {"x": 5285.93, "y": 2346.6, "z": 70.91}, {"x": 5285.2, "y": 2346.62, "z": 70.9}, {"x": 5284.48, "y": 2346.61, "z": 70.89}, {"x": 5283.65, "y": 2346.51, "z": 70.88}, {"x": 5282.79, "y": 2346.29, "z": 70.87}, {"x": 5281.93, "y": 2345.99, "z": 70.87}, {"x": 5281.09, "y": 2345.61, "z": 70.85}, {"x": 5280.26, "y": 2345.16, "z": 70.84}, {"x": 5279.47, "y": 2344.65, "z": 70.83}, {"x": 5278.7, "y": 2344.09, "z": 70.82}, {"x": 5277.97, "y": 2343.5, "z": 70.79}, {"x": 5277.28, "y": 2342.88, "z": 70.77}, {"x": 5276.63, "y": 2342.24, "z": 70.74}, {"x": 5276.03, "y": 2341.59, "z": 70.71}, {"x": 5275.49, "y": 2340.95, "z": 70.69}, {"x": 5275.01, "y": 2340.32, "z": 70.71}], "right_lane_mark_type": "NONE", "successors": [38109482], "predecessors": [38109234], "right_neighbor_id": null, "left_neighbor_id": null}, "38111604": {"id": 38111604, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5119.85, "y": 2452.02, "z": 64.44}, {"x": 5130.0, "y": 2445.16, "z": 64.86}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5117.9, "y": 2449.2, "z": 64.37}, {"x": 5120.85, "y": 2447.19, "z": 64.47}, {"x": 5121.09, "y": 2447.05, "z": 64.47}, {"x": 5121.3, "y": 2446.87, "z": 64.48}, {"x": 5121.29, "y": 2446.81, "z": 64.49}, {"x": 5130.0, "y": 2440.97, "z": 64.85}], "right_lane_mark_type": "NONE", "successors": [38111405], "predecessors": [38111774], "right_neighbor_id": null, "left_neighbor_id": 38111649}, "38111615": {"id": 38111615, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5113.84, "y": 2464.62, "z": 64.19}, {"x": 5108.05, "y": 2468.59, "z": 63.95}, {"x": 5102.35, "y": 2472.56, "z": 63.72}, {"x": 5096.35, "y": 2476.77, "z": 63.47}, {"x": 5094.43, "y": 2478.06, "z": 63.39}, {"x": 5091.61, "y": 2479.89, "z": 63.27}, {"x": 5089.72, "y": 2481.04, "z": 63.2}, {"x": 5088.43, "y": 2481.86, "z": 63.14}, {"x": 5087.28, "y": 2482.56, "z": 63.09}, {"x": 5086.34, "y": 2483.09, "z": 63.05}, {"x": 5085.14, "y": 2483.71, "z": 63.01}, {"x": 5084.23, "y": 2484.17, "z": 62.98}, {"x": 5083.14, "y": 2484.66, "z": 62.94}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5116.4, "y": 2468.26, "z": 64.25}, {"x": 5111.95, "y": 2471.45, "z": 64.09}, {"x": 5109.72, "y": 2473.07, "z": 64.02}, {"x": 5107.1, "y": 2474.87, "z": 63.9}, {"x": 5103.61, "y": 2477.16, "z": 63.76}, {"x": 5099.76, "y": 2479.78, "z": 63.6}, {"x": 5095.35, "y": 2482.63, "z": 63.41}, {"x": 5091.99, "y": 2484.45, "z": 63.28}, {"x": 5088.37, "y": 2486.26, "z": 63.15}, {"x": 5084.78, "y": 2487.92, "z": 63.01}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111165], "predecessors": [38111737], "right_neighbor_id": 38111330, "left_neighbor_id": 38111117}, "38111629": {"id": 38111629, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5150.88, "y": 2443.13, "z": 65.39}, {"x": 5145.11, "y": 2447.42, "z": 65.17}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5151.76, "y": 2444.37, "z": 65.42}, {"x": 5147.97, "y": 2447.04, "z": 65.29}, {"x": 5145.97, "y": 2448.56, "z": 65.22}], "right_lane_mark_type": "NONE", "successors": [38111126], "predecessors": [38111873, 38111278], "right_neighbor_id": null, "left_neighbor_id": 38111173}, "38111648": {"id": 38111648, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5117.46, "y": 2469.61, "z": 64.25}, {"x": 5116.6, "y": 2470.47, "z": 64.22}, {"x": 5116.53, "y": 2470.59, "z": 64.22}, {"x": 5116.2, "y": 2470.87, "z": 64.2}, {"x": 5113.51, "y": 2473.63, "z": 64.13}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5119.56, "y": 2472.48, "z": 64.2}, {"x": 5116.24, "y": 2475.72, "z": 64.08}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111000], "predecessors": [38111825, 38111228], "right_neighbor_id": null, "left_neighbor_id": null}, "38111649": {"id": 38111649, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5121.66, "y": 2454.65, "z": 64.48}, {"x": 5130.0, "y": 2449.04, "z": 64.84}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5119.85, "y": 2452.02, "z": 64.44}, {"x": 5130.0, "y": 2445.16, "z": 64.86}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111090], "predecessors": [38111781], "right_neighbor_id": 38111604, "left_neighbor_id": null}, "38111662": {"id": 38111662, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5180.46, "y": 2416.73, "z": 66.82}, {"x": 5166.03, "y": 2426.94, "z": 66.29}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5184.25, "y": 2421.43, "z": 66.78}, {"x": 5170.51, "y": 2430.82, "z": 66.28}, {"x": 5169.46, "y": 2431.57, "z": 66.22}], "right_lane_mark_type": "NONE", "successors": [38111278, 38111446, 38111175, 38111880], "predecessors": [38110982], "right_neighbor_id": null, "left_neighbor_id": null}, "38111695": {"id": 38111695, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5133.55, "y": 2446.6, "z": 64.97}, {"x": 5140.87, "y": 2441.56, "z": 65.24}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5131.72, "y": 2443.93, "z": 64.94}, {"x": 5139.07, "y": 2438.93, "z": 65.22}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38110986], "predecessors": [38111090], "right_neighbor_id": 38111212, "left_neighbor_id": null}, "38111696": {"id": 38111696, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5142.73, "y": 2444.2, "z": 65.25}, {"x": 5135.52, "y": 2449.51, "z": 64.99}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5145.11, "y": 2447.42, "z": 65.17}, {"x": 5137.92, "y": 2452.76, "z": 64.93}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111898, 38110983], "predecessors": [38111173], "right_neighbor_id": 38111126, "left_neighbor_id": 38111213}, "38111737": {"id": 38111737, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5122.33, "y": 2458.78, "z": 64.51}, {"x": 5121.22, "y": 2459.55, "z": 64.47}, {"x": 5113.84, "y": 2464.62, "z": 64.19}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5124.75, "y": 2462.33, "z": 64.55}, {"x": 5123.67, "y": 2463.07, "z": 64.52}, {"x": 5117.45, "y": 2467.5, "z": 64.29}, {"x": 5116.4, "y": 2468.26, "z": 64.25}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111615], "predecessors": [38111258], "right_neighbor_id": 38111786, "left_neighbor_id": 38111770}, "38111751": {"id": 38111751, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5079.26, "y": 2470.07, "z": 62.65}, {"x": 5082.01, "y": 2470.27, "z": 62.77}, {"x": 5084.24, "y": 2470.3, "z": 62.87}, {"x": 5085.56, "y": 2470.28, "z": 62.94}, {"x": 5086.7, "y": 2470.21, "z": 62.99}, {"x": 5087.88, "y": 2470.09, "z": 63.05}, {"x": 5089.17, "y": 2469.89, "z": 63.1}, {"x": 5091.33, "y": 2469.43, "z": 63.21}, {"x": 5093.3, "y": 2468.83, "z": 63.3}, {"x": 5095.12, "y": 2468.12, "z": 63.37}, {"x": 5096.85, "y": 2467.34, "z": 63.46}, {"x": 5098.62, "y": 2466.33, "z": 63.54}, {"x": 5100.04, "y": 2465.55, "z": 63.61}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5085.44, "y": 2465.44, "z": 62.84}, {"x": 5088.67, "y": 2465.36, "z": 63.0}, {"x": 5090.39, "y": 2465.13, "z": 63.08}, {"x": 5091.98, "y": 2464.8, "z": 63.17}, {"x": 5093.72, "y": 2464.32, "z": 63.25}, {"x": 5094.59, "y": 2464.01, "z": 63.31}, {"x": 5095.69, "y": 2463.57, "z": 63.37}, {"x": 5097.2, "y": 2462.94, "z": 63.44}, {"x": 5097.86, "y": 2462.61, "z": 63.48}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111774], "predecessors": [38111569], "right_neighbor_id": null, "left_neighbor_id": null}, "38111770": {"id": 38111770, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5120.49, "y": 2456.1, "z": 64.43}, {"x": 5112.0, "y": 2461.84, "z": 64.1}, {"x": 5111.95, "y": 2461.87, "z": 64.1}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5122.33, "y": 2458.78, "z": 64.51}, {"x": 5121.22, "y": 2459.55, "z": 64.47}, {"x": 5113.84, "y": 2464.62, "z": 64.19}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111117], "predecessors": [38111598], "right_neighbor_id": 38111737, "left_neighbor_id": null}, "38111774": {"id": 38111774, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5100.04, "y": 2465.55, "z": 63.61}, {"x": 5119.85, "y": 2452.02, "z": 64.44}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5097.86, "y": 2462.61, "z": 63.48}, {"x": 5098.44, "y": 2462.26, "z": 63.51}, {"x": 5098.89, "y": 2461.99, "z": 63.53}, {"x": 5100.26, "y": 2461.14, "z": 63.59}, {"x": 5102.01, "y": 2460.06, "z": 63.63}, {"x": 5103.45, "y": 2459.11, "z": 63.73}, {"x": 5107.42, "y": 2456.44, "z": 63.94}, {"x": 5110.5, "y": 2454.3, "z": 64.08}, {"x": 5117.9, "y": 2449.2, "z": 64.37}], "right_lane_mark_type": "NONE", "successors": [38111604], "predecessors": [38111259, 38111751], "right_neighbor_id": null, "left_neighbor_id": 38111781}, "38111781": {"id": 38111781, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5101.82, "y": 2468.17, "z": 63.68}, {"x": 5121.66, "y": 2454.65, "z": 64.48}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5100.04, "y": 2465.55, "z": 63.61}, {"x": 5119.85, "y": 2452.02, "z": 64.44}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111649], "predecessors": [38111425], "right_neighbor_id": 38111774, "left_neighbor_id": null}, "38111786": {"id": 38111786, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5124.75, "y": 2462.33, "z": 64.55}, {"x": 5123.67, "y": 2463.07, "z": 64.52}, {"x": 5117.45, "y": 2467.5, "z": 64.29}, {"x": 5116.4, "y": 2468.26, "z": 64.25}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5125.66, "y": 2463.59, "z": 64.52}, {"x": 5125.25, "y": 2463.86, "z": 64.51}, {"x": 5119.45, "y": 2468.02, "z": 64.33}, {"x": 5117.29, "y": 2469.51, "z": 64.25}], "right_lane_mark_type": "DASHED_WHITE", "successors": [38111330], "predecessors": [38111004], "right_neighbor_id": null, "left_neighbor_id": 38111737}, "38111825": {"id": 38111825, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5124.75, "y": 2462.33, "z": 64.55}, {"x": 5117.46, "y": 2469.61, "z": 64.25}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5125.66, "y": 2463.59, "z": 64.52}, {"x": 5119.56, "y": 2472.48, "z": 64.2}], "right_lane_mark_type": "NONE", "successors": [38111648], "predecessors": [38111004], "right_neighbor_id": null, "left_neighbor_id": null}, "38111849": {"id": 38111849, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5308.45, "y": 2323.82, "z": 71.8}, {"x": 5328.5, "y": 2310.17, "z": 72.07}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5307.63, "y": 2322.37, "z": 71.78}, {"x": 5327.54, "y": 2308.94, "z": 72.09}], "right_lane_mark_type": "NONE", "successors": [38111857], "predecessors": [38111861], "right_neighbor_id": null, "left_neighbor_id": 38109290}, "38111855": {"id": 38111855, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5308.45, "y": 2323.82, "z": 71.8}, {"x": 5309.12, "y": 2323.27, "z": 71.82}, {"x": 5309.82, "y": 2322.77, "z": 71.84}, {"x": 5310.56, "y": 2322.26, "z": 71.86}, {"x": 5311.35, "y": 2321.74, "z": 71.88}, {"x": 5312.17, "y": 2321.21, "z": 71.9}, {"x": 5313.01, "y": 2320.67, "z": 71.92}, {"x": 5313.87, "y": 2320.11, "z": 71.95}, {"x": 5314.74, "y": 2319.53, "z": 71.97}, {"x": 5315.6, "y": 2318.94, "z": 71.98}, {"x": 5316.44, "y": 2318.34, "z": 72.0}, {"x": 5317.27, "y": 2317.71, "z": 72.01}, {"x": 5318.06, "y": 2317.07, "z": 72.02}, {"x": 5318.82, "y": 2316.41, "z": 72.03}, {"x": 5319.52, "y": 2315.73, "z": 72.04}, {"x": 5320.16, "y": 2315.02, "z": 72.05}, {"x": 5320.74, "y": 2314.29, "z": 72.07}, {"x": 5321.24, "y": 2313.54, "z": 72.08}, {"x": 5321.65, "y": 2312.77, "z": 72.1}, {"x": 5321.97, "y": 2311.97, "z": 72.11}, {"x": 5322.19, "y": 2311.14, "z": 72.13}, {"x": 5322.29, "y": 2310.29, "z": 72.14}, {"x": 5322.27, "y": 2309.41, "z": 72.15}, {"x": 5322.11, "y": 2308.5, "z": 72.16}, {"x": 5321.82, "y": 2307.56, "z": 72.2}, {"x": 5321.42, "y": 2306.55, "z": 72.28}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5307.63, "y": 2322.37, "z": 71.78}, {"x": 5307.83, "y": 2322.21, "z": 71.79}, {"x": 5308.23, "y": 2321.9, "z": 71.8}, {"x": 5308.8, "y": 2321.46, "z": 71.82}, {"x": 5309.5, "y": 2320.9, "z": 71.84}, {"x": 5310.29, "y": 2320.24, "z": 71.86}, {"x": 5311.16, "y": 2319.5, "z": 71.89}, {"x": 5312.05, "y": 2318.69, "z": 71.91}, {"x": 5312.95, "y": 2317.84, "z": 71.94}, {"x": 5313.81, "y": 2316.95, "z": 71.97}, {"x": 5314.6, "y": 2316.05, "z": 71.99}, {"x": 5315.29, "y": 2315.15, "z": 72.01}, {"x": 5315.84, "y": 2314.28, "z": 72.03}, {"x": 5316.23, "y": 2313.43, "z": 72.06}, {"x": 5316.42, "y": 2312.64, "z": 72.09}, {"x": 5316.37, "y": 2311.92, "z": 72.13}, {"x": 5315.57, "y": 2310.62, "z": 72.21}], "right_lane_mark_type": "NONE", "successors": [38109302], "predecessors": [38111861], "right_neighbor_id": null, "left_neighbor_id": null}, "38111858": {"id": 38111858, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5272.94, "y": 2353.69, "z": 70.51}, {"x": 5285.11, "y": 2340.16, "z": 71.03}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5268.73, "y": 2346.16, "z": 70.46}, {"x": 5284.05, "y": 2338.62, "z": 71.01}], "right_lane_mark_type": "NONE", "successors": [38111866], "predecessors": [38117100], "right_neighbor_id": null, "left_neighbor_id": null}, "38111861": {"id": 38111861, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5303.12, "y": 2327.48, "z": 71.65}, {"x": 5308.45, "y": 2323.82, "z": 71.8}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 5302.21, "y": 2326.11, "z": 71.63}, {"x": 5307.63, "y": 2322.37, "z": 71.78}], "right_lane_mark_type": "NONE", "successors": [38111849, 38111855], "predecessors": [38111866], "right_neighbor_id": null, "left_neighbor_id": 38111103}, "38111866": {"id": 38111866, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5285.11, "y": 2340.16, "z": 71.03}, {"x": 5303.12, "y": 2327.48, "z": 71.65}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5284.05, "y": 2338.62, "z": 71.01}, {"x": 5288.66, "y": 2335.47, "z": 71.16}, {"x": 5302.21, "y": 2326.11, "z": 71.63}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111861], "predecessors": [38111858], "right_neighbor_id": null, "left_neighbor_id": 38109400}, "38111873": {"id": 38111873, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5166.37, "y": 2440.79, "z": 65.95}, {"x": 5165.77, "y": 2440.09, "z": 65.99}, {"x": 5165.03, "y": 2439.61, "z": 65.9}, {"x": 5164.25, "y": 2439.24, "z": 65.89}, {"x": 5163.45, "y": 2438.98, "z": 65.87}, {"x": 5162.64, "y": 2438.81, "z": 65.86}, {"x": 5161.8, "y": 2438.74, "z": 65.84}, {"x": 5160.95, "y": 2438.75, "z": 65.82}, {"x": 5160.09, "y": 2438.84, "z": 65.79}, {"x": 5159.22, "y": 2439.0, "z": 65.76}, {"x": 5158.35, "y": 2439.23, "z": 65.72}, {"x": 5157.48, "y": 2439.51, "z": 65.68}, {"x": 5156.61, "y": 2439.85, "z": 65.65}, {"x": 5155.74, "y": 2440.24, "z": 65.6}, {"x": 5154.89, "y": 2440.66, "z": 65.56}, {"x": 5154.05, "y": 2441.12, "z": 65.52}, {"x": 5153.22, "y": 2441.6, "z": 65.48}, {"x": 5152.42, "y": 2442.1, "z": 65.45}, {"x": 5150.88, "y": 2443.13, "z": 65.39}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5160.14, "y": 2445.81, "z": 65.74}, {"x": 5159.4, "y": 2444.83, "z": 65.71}, {"x": 5159.0, "y": 2444.27, "z": 65.67}, {"x": 5158.73, "y": 2443.96, "z": 65.67}, {"x": 5158.13, "y": 2443.33, "z": 65.63}, {"x": 5157.03, "y": 2442.76, "z": 65.56}, {"x": 5156.59, "y": 2442.67, "z": 65.55}, {"x": 5155.62, "y": 2442.36, "z": 65.53}, {"x": 5154.62, "y": 2442.67, "z": 65.54}, {"x": 5154.25, "y": 2442.73, "z": 65.52}, {"x": 5152.85, "y": 2443.67, "z": 65.47}, {"x": 5151.76, "y": 2444.37, "z": 65.42}], "right_lane_mark_type": "NONE", "successors": [38111629], "predecessors": [38119951], "right_neighbor_id": null, "left_neighbor_id": null}, "38111879": {"id": 38111879, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.37, "y": 2440.79, "z": 65.95}, {"x": 5165.82, "y": 2440.03, "z": 66.0}, {"x": 5165.32, "y": 2439.21, "z": 65.95}, {"x": 5164.85, "y": 2438.56, "z": 65.92}, {"x": 5164.31, "y": 2437.77, "z": 65.9}, {"x": 5163.79, "y": 2436.95, "z": 65.88}, {"x": 5163.3, "y": 2436.1, "z": 65.88}, {"x": 5162.86, "y": 2435.24, "z": 65.91}, {"x": 5162.47, "y": 2434.36, "z": 65.95}, {"x": 5162.14, "y": 2433.48, "z": 65.98}, {"x": 5161.89, "y": 2432.6, "z": 66.01}, {"x": 5161.72, "y": 2431.73, "z": 66.03}, {"x": 5161.64, "y": 2430.87, "z": 66.06}, {"x": 5161.68, "y": 2430.03, "z": 66.09}, {"x": 5161.83, "y": 2429.21, "z": 66.12}, {"x": 5162.1, "y": 2428.43, "z": 66.15}, {"x": 5162.51, "y": 2427.68, "z": 66.18}, {"x": 5163.07, "y": 2426.98, "z": 66.22}, {"x": 5163.79, "y": 2426.34, "z": 66.25}, {"x": 5164.69, "y": 2425.75, "z": 66.29}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5160.14, "y": 2445.81, "z": 65.74}, {"x": 5158.73, "y": 2443.96, "z": 65.67}, {"x": 5158.61, "y": 2443.83, "z": 65.66}, {"x": 5158.04, "y": 2442.97, "z": 65.6}, {"x": 5157.55, "y": 2442.18, "z": 65.58}, {"x": 5157.08, "y": 2441.36, "z": 65.59}, {"x": 5156.63, "y": 2440.52, "z": 65.61}, {"x": 5156.21, "y": 2439.65, "z": 65.64}, {"x": 5155.81, "y": 2438.77, "z": 65.67}, {"x": 5155.45, "y": 2437.87, "z": 65.69}, {"x": 5155.12, "y": 2436.96, "z": 65.71}, {"x": 5154.82, "y": 2436.03, "z": 65.75}, {"x": 5154.56, "y": 2435.1, "z": 65.77}, {"x": 5154.35, "y": 2434.17, "z": 65.79}, {"x": 5154.17, "y": 2433.23, "z": 65.81}, {"x": 5154.05, "y": 2432.3, "z": 65.83}, {"x": 5153.97, "y": 2431.37, "z": 65.84}, {"x": 5153.94, "y": 2430.45, "z": 65.85}, {"x": 5153.97, "y": 2429.54, "z": 65.86}, {"x": 5154.05, "y": 2428.64, "z": 65.86}, {"x": 5154.2, "y": 2427.75, "z": 65.87}, {"x": 5154.41, "y": 2426.89, "z": 65.88}, {"x": 5154.68, "y": 2426.05, "z": 65.9}, {"x": 5155.02, "y": 2425.23, "z": 65.9}, {"x": 5155.43, "y": 2424.44, "z": 65.91}, {"x": 5155.91, "y": 2423.68, "z": 65.93}, {"x": 5156.47, "y": 2422.95, "z": 65.95}, {"x": 5157.1, "y": 2422.26, "z": 66.02}, {"x": 5157.17, "y": 2422.2, "z": 66.01}, {"x": 5160.51, "y": 2419.95, "z": 66.15}], "right_lane_mark_type": "NONE", "successors": [38133154, 38133155], "predecessors": [38119951], "right_neighbor_id": null, "left_neighbor_id": null}, "38111880": {"id": 38111880, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.03, "y": 2426.94, "z": 66.29}, {"x": 5165.15, "y": 2427.4, "z": 66.26}, {"x": 5164.25, "y": 2427.72, "z": 66.23}, {"x": 5163.35, "y": 2427.9, "z": 66.2}, {"x": 5162.43, "y": 2427.96, "z": 66.18}, {"x": 5161.52, "y": 2427.91, "z": 66.15}, {"x": 5160.62, "y": 2427.75, "z": 66.11}, {"x": 5159.73, "y": 2427.49, "z": 66.08}, {"x": 5158.85, "y": 2427.14, "z": 66.06}, {"x": 5158.0, "y": 2426.71, "z": 66.02}, {"x": 5157.18, "y": 2426.22, "z": 65.99}, {"x": 5156.39, "y": 2425.66, "z": 65.95}, {"x": 5155.64, "y": 2425.05, "z": 65.93}, {"x": 5154.94, "y": 2424.4, "z": 65.89}, {"x": 5154.28, "y": 2423.71, "z": 65.86}, {"x": 5153.69, "y": 2423.0, "z": 65.82}, {"x": 5153.16, "y": 2422.28, "z": 65.79}, {"x": 5152.69, "y": 2421.54, "z": 65.79}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5169.46, "y": 2431.57, "z": 66.22}, {"x": 5168.51, "y": 2432.24, "z": 66.18}, {"x": 5167.8, "y": 2432.68, "z": 66.15}, {"x": 5166.94, "y": 2433.12, "z": 66.04}, {"x": 5166.06, "y": 2433.5, "z": 66.03}, {"x": 5165.16, "y": 2433.8, "z": 65.99}, {"x": 5164.25, "y": 2434.04, "z": 65.97}, {"x": 5163.33, "y": 2434.22, "z": 65.96}, {"x": 5162.39, "y": 2434.33, "z": 65.95}, {"x": 5161.46, "y": 2434.37, "z": 65.94}, {"x": 5160.51, "y": 2434.36, "z": 65.93}, {"x": 5159.57, "y": 2434.29, "z": 65.91}, {"x": 5158.63, "y": 2434.17, "z": 65.89}, {"x": 5157.7, "y": 2433.99, "z": 65.88}, {"x": 5156.77, "y": 2433.76, "z": 65.86}, {"x": 5155.85, "y": 2433.48, "z": 65.85}, {"x": 5154.95, "y": 2433.15, "z": 65.84}, {"x": 5154.05, "y": 2432.78, "z": 65.81}, {"x": 5153.18, "y": 2432.36, "z": 65.8}, {"x": 5152.33, "y": 2431.9, "z": 65.78}, {"x": 5151.5, "y": 2431.39, "z": 65.76}, {"x": 5150.7, "y": 2430.85, "z": 65.74}, {"x": 5149.93, "y": 2430.27, "z": 65.71}, {"x": 5149.18, "y": 2429.65, "z": 65.68}, {"x": 5148.47, "y": 2429.01, "z": 65.65}, {"x": 5147.8, "y": 2428.33, "z": 65.63}, {"x": 5147.17, "y": 2427.62, "z": 65.6}, {"x": 5146.58, "y": 2426.88, "z": 65.6}, {"x": 5146.08, "y": 2426.08, "z": 65.61}], "right_lane_mark_type": "NONE", "successors": [38111342], "predecessors": [38111662], "right_neighbor_id": null, "left_neighbor_id": null}, "38111884": {"id": 38111884, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.37, "y": 2440.79, "z": 65.95}, {"x": 5165.73, "y": 2439.91, "z": 65.99}, {"x": 5164.94, "y": 2439.21, "z": 65.91}, {"x": 5164.14, "y": 2438.58, "z": 65.89}, {"x": 5163.33, "y": 2438.04, "z": 65.88}, {"x": 5162.5, "y": 2437.57, "z": 65.87}, {"x": 5161.66, "y": 2437.18, "z": 65.86}, {"x": 5160.82, "y": 2436.86, "z": 65.86}, {"x": 5159.96, "y": 2436.61, "z": 65.85}, {"x": 5159.11, "y": 2436.43, "z": 65.84}, {"x": 5158.25, "y": 2436.32, "z": 65.82}, {"x": 5157.39, "y": 2436.28, "z": 65.8}, {"x": 5156.54, "y": 2436.31, "z": 65.77}, {"x": 5155.69, "y": 2436.39, "z": 65.75}, {"x": 5154.86, "y": 2436.54, "z": 65.72}, {"x": 5154.03, "y": 2436.76, "z": 65.69}, {"x": 5153.21, "y": 2437.03, "z": 65.67}, {"x": 5152.41, "y": 2437.35, "z": 65.64}, {"x": 5151.62, "y": 2437.74, "z": 65.6}, {"x": 5150.86, "y": 2438.18, "z": 65.57}, {"x": 5150.12, "y": 2438.67, "z": 65.53}, {"x": 5149.4, "y": 2439.21, "z": 65.5}, {"x": 5148.71, "y": 2439.81, "z": 65.47}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5160.14, "y": 2445.81, "z": 65.74}, {"x": 5159.61, "y": 2444.88, "z": 65.7}, {"x": 5159.05, "y": 2444.09, "z": 65.66}, {"x": 5158.46, "y": 2443.44, "z": 65.64}, {"x": 5157.83, "y": 2442.93, "z": 65.6}, {"x": 5157.17, "y": 2442.54, "z": 65.56}, {"x": 5156.47, "y": 2442.27, "z": 65.54}, {"x": 5155.75, "y": 2442.11, "z": 65.52}, {"x": 5155.0, "y": 2442.06, "z": 65.5}, {"x": 5154.22, "y": 2442.1, "z": 65.49}, {"x": 5153.42, "y": 2442.24, "z": 65.47}, {"x": 5152.6, "y": 2442.46, "z": 65.44}, {"x": 5151.75, "y": 2442.76, "z": 65.41}, {"x": 5150.88, "y": 2443.13, "z": 65.39}], "right_lane_mark_type": "NONE", "successors": [38111173], "predecessors": [38119951], "right_neighbor_id": null, "left_neighbor_id": null}, "38111894": {"id": 38111894, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5288.86, "y": 2345.49, "z": 70.99}, {"x": 5272.94, "y": 2353.69, "z": 70.51}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5289.51, "y": 2346.62, "z": 70.98}, {"x": 5275.94, "y": 2358.54, "z": 70.49}], "right_lane_mark_type": "NONE", "successors": [38109382], "predecessors": [38111904], "right_neighbor_id": null, "left_neighbor_id": null}, "38111898": {"id": 38111898, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5135.52, "y": 2449.51, "z": 64.99}, {"x": 5131.81, "y": 2454.6, "z": 64.83}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5137.92, "y": 2452.76, "z": 64.93}, {"x": 5133.92, "y": 2458.31, "z": 64.74}], "right_lane_mark_type": "NONE", "successors": [38114770], "predecessors": [38111696], "right_neighbor_id": null, "left_neighbor_id": null}, "38111902": {"id": 38111902, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5332.07, "y": 2315.53, "z": 72.0}, {"x": 5312.04, "y": 2329.04, "z": 71.8}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5332.94, "y": 2316.73, "z": 71.94}, {"x": 5312.95, "y": 2330.28, "z": 71.79}], "right_lane_mark_type": "NONE", "successors": [38111905], "predecessors": [38111899], "right_neighbor_id": null, "left_neighbor_id": 38109397}, "38111904": {"id": 38111904, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5306.47, "y": 2332.86, "z": 71.62}, {"x": 5288.86, "y": 2345.49, "z": 70.99}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5307.33, "y": 2334.12, "z": 71.6}, {"x": 5289.51, "y": 2346.62, "z": 70.98}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111894, 38111937], "predecessors": [38111905], "right_neighbor_id": null, "left_neighbor_id": 38109234}, "38111905": {"id": 38111905, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5312.04, "y": 2329.04, "z": 71.8}, {"x": 5306.47, "y": 2332.86, "z": 71.62}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5312.95, "y": 2330.28, "z": 71.79}, {"x": 5308.42, "y": 2333.35, "z": 71.64}, {"x": 5307.33, "y": 2334.12, "z": 71.6}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111904], "predecessors": [38111902, 38111935], "right_neighbor_id": null, "left_neighbor_id": 38111133}, "38111935": {"id": 38111935, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5327.53, "y": 2329.17, "z": 72.01}, {"x": 5326.98, "y": 2328.13, "z": 72.01}, {"x": 5326.64, "y": 2327.62, "z": 72.01}, {"x": 5326.21, "y": 2327.26, "z": 71.98}, {"x": 5325.72, "y": 2326.92, "z": 71.97}, {"x": 5325.17, "y": 2326.6, "z": 71.96}, {"x": 5324.56, "y": 2326.31, "z": 71.96}, {"x": 5323.89, "y": 2326.06, "z": 71.97}, {"x": 5323.18, "y": 2325.86, "z": 71.99}, {"x": 5322.4, "y": 2325.71, "z": 71.99}, {"x": 5321.58, "y": 2325.63, "z": 71.98}, {"x": 5320.7, "y": 2325.62, "z": 71.97}, {"x": 5319.78, "y": 2325.68, "z": 71.97}, {"x": 5318.81, "y": 2325.84, "z": 71.97}, {"x": 5317.79, "y": 2326.09, "z": 71.96}, {"x": 5316.72, "y": 2326.44, "z": 71.94}, {"x": 5315.61, "y": 2326.9, "z": 71.91}, {"x": 5314.46, "y": 2327.48, "z": 71.88}, {"x": 5313.27, "y": 2328.19, "z": 71.84}, {"x": 5312.04, "y": 2329.04, "z": 71.8}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5320.76, "y": 2333.27, "z": 71.87}, {"x": 5319.86, "y": 2331.41, "z": 71.85}, {"x": 5319.14, "y": 2330.69, "z": 71.84}, {"x": 5318.37, "y": 2330.4, "z": 71.83}, {"x": 5317.58, "y": 2330.3, "z": 71.83}, {"x": 5315.46, "y": 2330.24, "z": 71.83}, {"x": 5314.48, "y": 2330.23, "z": 71.82}, {"x": 5313.62, "y": 2330.28, "z": 71.8}, {"x": 5312.95, "y": 2330.28, "z": 71.79}], "right_lane_mark_type": "NONE", "successors": [38111905], "predecessors": [38109176], "right_neighbor_id": null, "left_neighbor_id": null}, "38111937": {"id": 38111937, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 5288.86, "y": 2345.49, "z": 70.99}, {"x": 5288.23, "y": 2345.66, "z": 70.98}, {"x": 5287.57, "y": 2345.63, "z": 70.97}, {"x": 5286.87, "y": 2345.42, "z": 70.96}, {"x": 5286.15, "y": 2345.04, "z": 70.96}, {"x": 5285.42, "y": 2344.54, "z": 70.97}, {"x": 5284.69, "y": 2343.92, "z": 70.98}, {"x": 5283.97, "y": 2343.21, "z": 70.97}, {"x": 5283.25, "y": 2342.44, "z": 70.96}, {"x": 5282.56, "y": 2341.63, "z": 70.95}, {"x": 5281.91, "y": 2340.8, "z": 70.94}, {"x": 5281.3, "y": 2339.97, "z": 70.92}, {"x": 5280.73, "y": 2339.17, "z": 70.9}, {"x": 5280.23, "y": 2338.43, "z": 70.87}, {"x": 5279.8, "y": 2337.75, "z": 70.88}, {"x": 5279.45, "y": 2337.18, "z": 70.9}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5289.51, "y": 2346.62, "z": 70.98}, {"x": 5289.27, "y": 2346.88, "z": 70.96}, {"x": 5288.96, "y": 2347.13, "z": 70.96}, {"x": 5288.59, "y": 2347.37, "z": 70.94}, {"x": 5288.16, "y": 2347.57, "z": 70.93}, {"x": 5287.66, "y": 2347.73, "z": 70.91}, {"x": 5287.11, "y": 2347.84, "z": 70.9}, {"x": 5286.5, "y": 2347.9, "z": 70.89}, {"x": 5285.83, "y": 2347.88, "z": 70.88}, {"x": 5285.1, "y": 2347.78, "z": 70.86}, {"x": 5284.32, "y": 2347.6, "z": 70.85}, {"x": 5283.48, "y": 2347.32, "z": 70.85}, {"x": 5282.59, "y": 2346.92, "z": 70.85}, {"x": 5281.65, "y": 2346.41, "z": 70.85}, {"x": 5280.67, "y": 2345.78, "z": 70.84}, {"x": 5279.63, "y": 2345.0, "z": 70.83}, {"x": 5278.54, "y": 2344.08, "z": 70.81}, {"x": 5277.41, "y": 2342.99, "z": 70.77}, {"x": 5276.23, "y": 2341.75, "z": 70.72}, {"x": 5275.01, "y": 2340.32, "z": 70.71}], "right_lane_mark_type": "NONE", "successors": [38109482], "predecessors": [38111904], "right_neighbor_id": null, "left_neighbor_id": null}, "38114309": {"id": 38114309, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5224.76, "y": 2328.95, "z": 70.43}, {"x": 5219.69, "y": 2332.21, "z": 70.08}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5226.56, "y": 2331.45, "z": 70.42}, {"x": 5221.28, "y": 2334.87, "z": 69.99}], "right_lane_mark_type": "NONE", "successors": [38114427, 38114403], "predecessors": [38115208], "right_neighbor_id": null, "left_neighbor_id": null}, "38114318": {"id": 38114318, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5230.74, "y": 2365.5, "z": 69.44}, {"x": 5232.17, "y": 2367.83, "z": 69.33}, {"x": 5232.45, "y": 2368.38, "z": 69.32}, {"x": 5232.61, "y": 2368.82, "z": 69.31}, {"x": 5232.75, "y": 2369.41, "z": 69.29}, {"x": 5232.82, "y": 2370.07, "z": 69.27}, {"x": 5232.86, "y": 2370.8, "z": 69.25}, {"x": 5232.88, "y": 2371.77, "z": 69.25}, {"x": 5232.87, "y": 2372.57, "z": 69.25}, {"x": 5232.8, "y": 2373.73, "z": 69.24}, {"x": 5232.74, "y": 2374.61, "z": 69.24}, {"x": 5232.68, "y": 2375.56, "z": 69.23}, {"x": 5232.56, "y": 2376.49, "z": 69.21}, {"x": 5232.39, "y": 2377.39, "z": 69.18}, {"x": 5232.17, "y": 2378.26, "z": 69.16}, {"x": 5231.9, "y": 2379.1, "z": 69.14}, {"x": 5231.59, "y": 2379.91, "z": 69.11}, {"x": 5231.24, "y": 2380.69, "z": 69.08}, {"x": 5230.84, "y": 2381.42, "z": 69.05}, {"x": 5230.41, "y": 2382.12, "z": 69.03}, {"x": 5229.93, "y": 2382.77, "z": 69.0}, {"x": 5229.42, "y": 2383.38, "z": 68.98}, {"x": 5228.87, "y": 2383.94, "z": 68.95}, {"x": 5228.29, "y": 2384.45, "z": 68.92}, {"x": 5227.04, "y": 2385.3, "z": 68.87}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5235.67, "y": 2362.4, "z": 69.55}, {"x": 5236.16, "y": 2363.28, "z": 69.51}, {"x": 5236.6, "y": 2364.18, "z": 69.49}, {"x": 5237.0, "y": 2365.1, "z": 69.44}, {"x": 5237.34, "y": 2366.04, "z": 69.43}, {"x": 5237.64, "y": 2367.0, "z": 69.41}, {"x": 5237.9, "y": 2367.97, "z": 69.41}, {"x": 5238.11, "y": 2368.94, "z": 69.42}, {"x": 5238.28, "y": 2369.93, "z": 69.43}, {"x": 5238.4, "y": 2370.92, "z": 69.43}, {"x": 5238.48, "y": 2371.92, "z": 69.43}, {"x": 5238.52, "y": 2372.91, "z": 69.42}, {"x": 5238.52, "y": 2373.91, "z": 69.4}, {"x": 5238.47, "y": 2374.9, "z": 69.38}, {"x": 5238.39, "y": 2375.88, "z": 69.37}, {"x": 5238.26, "y": 2376.86, "z": 69.34}, {"x": 5238.1, "y": 2377.82, "z": 69.3}, {"x": 5237.9, "y": 2378.77, "z": 69.27}, {"x": 5237.66, "y": 2379.7, "z": 69.24}, {"x": 5237.39, "y": 2380.62, "z": 69.2}, {"x": 5237.08, "y": 2381.51, "z": 69.16}, {"x": 5236.73, "y": 2382.38, "z": 69.12}, {"x": 5236.35, "y": 2383.22, "z": 69.09}, {"x": 5235.93, "y": 2384.04, "z": 69.05}, {"x": 5235.48, "y": 2384.82, "z": 69.02}, {"x": 5235.0, "y": 2385.57, "z": 68.98}, {"x": 5234.49, "y": 2386.29, "z": 68.95}, {"x": 5233.94, "y": 2386.97, "z": 68.91}, {"x": 5233.37, "y": 2387.6, "z": 68.88}, {"x": 5232.76, "y": 2388.2, "z": 68.85}, {"x": 5232.13, "y": 2388.75, "z": 68.84}, {"x": 5231.35, "y": 2389.25, "z": 68.83}, {"x": 5230.31, "y": 2389.95, "z": 68.79}, {"x": 5230.05, "y": 2390.09, "z": 68.78}], "right_lane_mark_type": "NONE", "successors": [38114436], "predecessors": [38114351], "right_neighbor_id": null, "left_neighbor_id": null}, "38114332": {"id": 38114332, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5237.88, "y": 2391.1, "z": 68.99}, {"x": 5244.23, "y": 2400.0, "z": 69.18}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5242.29, "y": 2387.75, "z": 69.16}, {"x": 5251.14, "y": 2400.0, "z": 69.24}], "right_lane_mark_type": "NONE", "successors": [38109824], "predecessors": [38114376, 38114428, 38114405], "right_neighbor_id": null, "left_neighbor_id": null}, "38114334": {"id": 38114334, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5221.27, "y": 2334.88, "z": 69.99}, {"x": 5221.91, "y": 2334.46, "z": 70.04}, {"x": 5226.56, "y": 2331.45, "z": 70.42}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5219.7, "y": 2332.2, "z": 70.08}, {"x": 5224.76, "y": 2328.95, "z": 70.43}], "right_lane_mark_type": "NONE", "successors": [38115671], "predecessors": [38114390, 38114407], "right_neighbor_id": null, "left_neighbor_id": null}, "38114335": {"id": 38114335, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5208.47, "y": 2336.18, "z": 69.85}, {"x": 5204.79, "y": 2338.45, "z": 69.67}, {"x": 5174.16, "y": 2356.73, "z": 67.63}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5210.02, "y": 2338.55, "z": 69.89}, {"x": 5191.39, "y": 2349.92, "z": 68.75}, {"x": 5175.92, "y": 2359.61, "z": 67.7}], "right_lane_mark_type": "NONE", "successors": [38120769, 38119984], "predecessors": [38114427, 38114348], "right_neighbor_id": null, "left_neighbor_id": null}, "38114336": {"id": 38114336, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5154.86, "y": 2370.0, "z": 66.34}, {"x": 5167.66, "y": 2362.35, "z": 67.19}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5148.63, "y": 2370.0, "z": 66.08}, {"x": 5166.06, "y": 2359.67, "z": 67.17}], "right_lane_mark_type": "NONE", "successors": [38120167, 38120430], "predecessors": [38119868], "right_neighbor_id": null, "left_neighbor_id": null}, "38114340": {"id": 38114340, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5245.56, "y": 2372.45, "z": 69.59}, {"x": 5227.04, "y": 2385.3, "z": 68.87}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5249.22, "y": 2376.88, "z": 69.58}, {"x": 5241.62, "y": 2382.07, "z": 69.22}, {"x": 5230.05, "y": 2390.09, "z": 68.78}], "right_lane_mark_type": "NONE", "successors": [38114436], "predecessors": [38116085], "right_neighbor_id": null, "left_neighbor_id": null}, "38114347": {"id": 38114347, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5208.92, "y": 2329.99, "z": 69.95}, {"x": 5210.11, "y": 2331.86, "z": 69.9}, {"x": 5214.63, "y": 2339.32, "z": 69.86}, {"x": 5215.0, "y": 2339.91, "z": 69.83}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5213.9, "y": 2326.93, "z": 70.07}, {"x": 5217.01, "y": 2332.02, "z": 70.01}, {"x": 5220.1, "y": 2337.01, "z": 69.89}], "right_lane_mark_type": "NONE", "successors": [38114351], "predecessors": [38114410], "right_neighbor_id": null, "left_neighbor_id": null}, "38114348": {"id": 38114348, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5208.92, "y": 2329.99, "z": 69.95}, {"x": 5209.32, "y": 2330.64, "z": 69.94}, {"x": 5209.46, "y": 2330.95, "z": 69.93}, {"x": 5209.62, "y": 2331.23, "z": 69.92}, {"x": 5209.98, "y": 2332.34, "z": 69.89}, {"x": 5209.92, "y": 2334.19, "z": 69.85}, {"x": 5209.62, "y": 2334.91, "z": 69.85}, {"x": 5209.24, "y": 2335.48, "z": 69.86}, {"x": 5208.83, "y": 2335.91, "z": 69.86}, {"x": 5208.47, "y": 2336.18, "z": 69.85}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5213.9, "y": 2326.93, "z": 70.07}, {"x": 5214.04, "y": 2327.15, "z": 70.07}, {"x": 5214.15, "y": 2327.34, "z": 70.07}, {"x": 5214.54, "y": 2328.11, "z": 70.06}, {"x": 5214.92, "y": 2329.3, "z": 70.03}, {"x": 5215.08, "y": 2330.47, "z": 70.01}, {"x": 5215.2, "y": 2331.54, "z": 70.0}, {"x": 5214.88, "y": 2332.67, "z": 69.97}, {"x": 5214.58, "y": 2333.69, "z": 69.94}, {"x": 5214.16, "y": 2334.64, "z": 69.9}, {"x": 5213.66, "y": 2335.51, "z": 69.87}, {"x": 5213.11, "y": 2336.29, "z": 69.84}, {"x": 5212.53, "y": 2336.98, "z": 69.83}, {"x": 5212.14, "y": 2337.35, "z": 69.84}, {"x": 5211.67, "y": 2337.71, "z": 69.86}, {"x": 5211.18, "y": 2337.96, "z": 69.9}, {"x": 5210.95, "y": 2338.04, "z": 69.9}, {"x": 5210.4, "y": 2338.36, "z": 69.9}, {"x": 5210.02, "y": 2338.55, "z": 69.89}], "right_lane_mark_type": "NONE", "successors": [38114335], "predecessors": [38114410], "right_neighbor_id": null, "left_neighbor_id": null}, "38114349": {"id": 38114349, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5220.0, "y": 2390.12, "z": 68.56}, {"x": 5227.04, "y": 2385.3, "z": 68.87}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5220.0, "y": 2386.47, "z": 68.63}, {"x": 5225.39, "y": 2382.69, "z": 68.88}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38114428], "predecessors": [38114426], "right_neighbor_id": 38114404, "left_neighbor_id": 38114436}, "38114351": {"id": 38114351, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5215.0, "y": 2339.91, "z": 69.83}, {"x": 5230.74, "y": 2365.5, "z": 69.44}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5220.1, "y": 2337.01, "z": 69.89}, {"x": 5235.67, "y": 2362.4, "z": 69.55}], "right_lane_mark_type": "NONE", "successors": [38114318, 38114446, 38114405], "predecessors": [38114355, 38114347, 38114403], "right_neighbor_id": null, "left_neighbor_id": null}, "38114355": {"id": 38114355, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5209.29, "y": 2337.42, "z": 69.87}, {"x": 5209.76, "y": 2337.16, "z": 69.87}, {"x": 5210.33, "y": 2337.03, "z": 69.87}, {"x": 5210.93, "y": 2337.04, "z": 69.86}, {"x": 5211.71, "y": 2337.17, "z": 69.84}, {"x": 5212.46, "y": 2337.38, "z": 69.84}, {"x": 5213.05, "y": 2337.67, "z": 69.84}, {"x": 5213.68, "y": 2338.15, "z": 69.85}, {"x": 5214.2, "y": 2338.64, "z": 69.87}, {"x": 5214.63, "y": 2339.32, "z": 69.86}, {"x": 5215.0, "y": 2339.91, "z": 69.83}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5207.69, "y": 2334.98, "z": 69.87}, {"x": 5208.41, "y": 2334.54, "z": 69.87}, {"x": 5209.04, "y": 2334.21, "z": 69.86}, {"x": 5209.82, "y": 2333.91, "z": 69.85}, {"x": 5210.65, "y": 2333.66, "z": 69.86}, {"x": 5211.86, "y": 2333.5, "z": 69.89}, {"x": 5213.16, "y": 2333.47, "z": 69.91}, {"x": 5214.42, "y": 2333.63, "z": 69.94}, {"x": 5215.47, "y": 2333.86, "z": 69.95}, {"x": 5216.42, "y": 2334.17, "z": 69.95}, {"x": 5217.5, "y": 2334.66, "z": 69.94}, {"x": 5218.4, "y": 2335.31, "z": 69.92}, {"x": 5219.28, "y": 2336.07, "z": 69.9}, {"x": 5220.1, "y": 2337.01, "z": 69.89}], "right_lane_mark_type": "NONE", "successors": [38114351], "predecessors": [38120064], "right_neighbor_id": null, "left_neighbor_id": null}, "38114374": {"id": 38114374, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5225.39, "y": 2382.69, "z": 68.88}, {"x": 5244.11, "y": 2370.36, "z": 69.6}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5222.35, "y": 2377.97, "z": 68.82}, {"x": 5240.78, "y": 2365.26, "z": 69.5}], "right_lane_mark_type": "NONE", "successors": [38109359], "predecessors": [38114404], "right_neighbor_id": null, "left_neighbor_id": null}, "38114376": {"id": 38114376, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5245.56, "y": 2372.45, "z": 69.59}, {"x": 5244.29, "y": 2373.34, "z": 69.55}, {"x": 5243.1, "y": 2374.18, "z": 69.5}, {"x": 5241.99, "y": 2374.97, "z": 69.46}, {"x": 5240.96, "y": 2375.73, "z": 69.42}, {"x": 5240.01, "y": 2376.45, "z": 69.39}, {"x": 5239.15, "y": 2377.14, "z": 69.35}, {"x": 5238.36, "y": 2377.8, "z": 69.31}, {"x": 5237.65, "y": 2378.44, "z": 69.28}, {"x": 5237.02, "y": 2379.06, "z": 69.25}, {"x": 5236.46, "y": 2379.66, "z": 69.22}, {"x": 5235.99, "y": 2380.26, "z": 69.19}, {"x": 5235.59, "y": 2380.85, "z": 69.16}, {"x": 5235.27, "y": 2381.45, "z": 69.14}, {"x": 5235.02, "y": 2382.04, "z": 69.12}, {"x": 5234.85, "y": 2382.65, "z": 69.09}, {"x": 5234.75, "y": 2383.27, "z": 69.07}, {"x": 5234.73, "y": 2383.9, "z": 69.05}, {"x": 5234.79, "y": 2384.56, "z": 69.02}, {"x": 5234.91, "y": 2385.25, "z": 69.0}, {"x": 5235.11, "y": 2385.96, "z": 68.97}, {"x": 5235.38, "y": 2386.71, "z": 68.95}, {"x": 5235.73, "y": 2387.5, "z": 68.93}, {"x": 5236.14, "y": 2388.34, "z": 68.93}, {"x": 5236.63, "y": 2389.22, "z": 68.94}, {"x": 5237.18, "y": 2390.16, "z": 68.95}, {"x": 5237.88, "y": 2391.1, "z": 68.99}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5249.22, "y": 2376.88, "z": 69.58}, {"x": 5244.85, "y": 2379.86, "z": 69.36}, {"x": 5242.02, "y": 2381.8, "z": 69.25}, {"x": 5241.62, "y": 2382.07, "z": 69.22}, {"x": 5241.07, "y": 2382.56, "z": 69.16}, {"x": 5240.75, "y": 2383.04, "z": 69.14}, {"x": 5240.58, "y": 2383.52, "z": 69.12}, {"x": 5240.48, "y": 2384.4, "z": 69.11}, {"x": 5240.58, "y": 2385.05, "z": 69.13}, {"x": 5240.81, "y": 2385.58, "z": 69.12}, {"x": 5241.24, "y": 2386.18, "z": 69.13}, {"x": 5242.29, "y": 2387.75, "z": 69.16}], "right_lane_mark_type": "NONE", "successors": [38114332], "predecessors": [38116085], "right_neighbor_id": null, "left_neighbor_id": null}, "38114390": {"id": 38114390, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5208.92, "y": 2329.99, "z": 69.95}, {"x": 5209.85, "y": 2331.47, "z": 69.91}, {"x": 5210.73, "y": 2332.74, "z": 69.88}, {"x": 5211.56, "y": 2333.82, "z": 69.87}, {"x": 5212.34, "y": 2334.72, "z": 69.87}, {"x": 5213.08, "y": 2335.45, "z": 69.86}, {"x": 5213.79, "y": 2336.01, "z": 69.87}, {"x": 5214.46, "y": 2336.44, "z": 69.87}, {"x": 5215.11, "y": 2336.73, "z": 69.87}, {"x": 5215.74, "y": 2336.9, "z": 69.89}, {"x": 5216.35, "y": 2336.96, "z": 69.9}, {"x": 5216.95, "y": 2336.93, "z": 69.91}, {"x": 5217.55, "y": 2336.81, "z": 69.91}, {"x": 5218.15, "y": 2336.61, "z": 69.9}, {"x": 5218.75, "y": 2336.36, "z": 69.9}, {"x": 5219.37, "y": 2336.06, "z": 69.9}, {"x": 5219.99, "y": 2335.69, "z": 69.91}, {"x": 5221.27, "y": 2334.88, "z": 69.99}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5213.9, "y": 2326.93, "z": 70.07}, {"x": 5217.0, "y": 2332.01, "z": 70.01}, {"x": 5217.42, "y": 2332.58, "z": 70.0}, {"x": 5217.74, "y": 2332.81, "z": 69.99}, {"x": 5218.02, "y": 2332.89, "z": 69.98}, {"x": 5218.35, "y": 2332.87, "z": 70.0}, {"x": 5218.66, "y": 2332.78, "z": 70.01}, {"x": 5219.7, "y": 2332.2, "z": 70.08}], "right_lane_mark_type": "NONE", "successors": [38114334], "predecessors": [38114410], "right_neighbor_id": null, "left_neighbor_id": null}, "38114403": {"id": 38114403, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5219.69, "y": 2332.21, "z": 70.08}, {"x": 5218.66, "y": 2332.78, "z": 70.01}, {"x": 5216.8, "y": 2333.81, "z": 69.96}, {"x": 5215.83, "y": 2334.63, "z": 69.94}, {"x": 5215.17, "y": 2335.47, "z": 69.91}, {"x": 5214.75, "y": 2336.29, "z": 69.88}, {"x": 5214.53, "y": 2337.08, "z": 69.86}, {"x": 5214.46, "y": 2337.8, "z": 69.84}, {"x": 5214.5, "y": 2338.45, "z": 69.85}, {"x": 5214.61, "y": 2339.0, "z": 69.84}, {"x": 5214.75, "y": 2339.42, "z": 69.85}, {"x": 5215.0, "y": 2339.91, "z": 69.83}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5221.28, "y": 2334.87, "z": 69.99}, {"x": 5220.03, "y": 2335.68, "z": 69.91}, {"x": 5219.78, "y": 2335.92, "z": 69.9}, {"x": 5219.71, "y": 2336.17, "z": 69.9}, {"x": 5219.79, "y": 2336.46, "z": 69.89}, {"x": 5220.1, "y": 2337.01, "z": 69.89}], "right_lane_mark_type": "NONE", "successors": [38114351], "predecessors": [38114309], "right_neighbor_id": null, "left_neighbor_id": null}, "38114404": {"id": 38114404, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5220.0, "y": 2386.47, "z": 68.63}, {"x": 5225.39, "y": 2382.69, "z": 68.88}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5220.0, "y": 2379.6, "z": 68.7}, {"x": 5222.35, "y": 2377.97, "z": 68.82}], "right_lane_mark_type": "NONE", "successors": [38114374], "predecessors": [38114433], "right_neighbor_id": null, "left_neighbor_id": 38114349}, "38114405": {"id": 38114405, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5230.74, "y": 2365.5, "z": 69.44}, {"x": 5233.22, "y": 2369.26, "z": 69.29}, {"x": 5233.47, "y": 2369.75, "z": 69.29}, {"x": 5233.61, "y": 2370.29, "z": 69.28}, {"x": 5233.69, "y": 2371.03, "z": 69.29}, {"x": 5234.7, "y": 2383.75, "z": 69.05}, {"x": 5234.82, "y": 2384.84, "z": 69.01}, {"x": 5235.03, "y": 2385.74, "z": 68.98}, {"x": 5235.36, "y": 2386.65, "z": 68.95}, {"x": 5235.79, "y": 2387.65, "z": 68.93}, {"x": 5236.25, "y": 2388.52, "z": 68.93}, {"x": 5236.71, "y": 2389.36, "z": 68.94}, {"x": 5237.29, "y": 2390.3, "z": 68.96}, {"x": 5237.88, "y": 2391.1, "z": 68.99}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5235.67, "y": 2362.4, "z": 69.55}, {"x": 5237.11, "y": 2364.92, "z": 69.44}, {"x": 5237.62, "y": 2366.33, "z": 69.42}, {"x": 5237.85, "y": 2367.31, "z": 69.41}, {"x": 5238.16, "y": 2369.27, "z": 69.43}, {"x": 5239.18, "y": 2382.96, "z": 69.1}, {"x": 5239.38, "y": 2384.02, "z": 69.07}, {"x": 5239.78, "y": 2384.76, "z": 69.07}, {"x": 5240.58, "y": 2385.72, "z": 69.1}, {"x": 5242.29, "y": 2387.75, "z": 69.16}], "right_lane_mark_type": "NONE", "successors": [38114332], "predecessors": [38114351], "right_neighbor_id": null, "left_neighbor_id": null}, "38114407": {"id": 38114407, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5209.29, "y": 2337.42, "z": 69.87}, {"x": 5210.4, "y": 2337.07, "z": 69.87}, {"x": 5219.42, "y": 2335.73, "z": 69.91}, {"x": 5221.27, "y": 2334.88, "z": 69.99}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5207.69, "y": 2334.98, "z": 69.87}, {"x": 5209.21, "y": 2334.34, "z": 69.85}, {"x": 5218.35, "y": 2332.87, "z": 70.0}, {"x": 5218.66, "y": 2332.78, "z": 70.01}, {"x": 5219.7, "y": 2332.2, "z": 70.08}], "right_lane_mark_type": "NONE", "successors": [38114334], "predecessors": [38120064], "right_neighbor_id": null, "left_neighbor_id": null}, "38114410": {"id": 38114410, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5190.0, "y": 2298.9, "z": 70.41}, {"x": 5192.4, "y": 2302.84, "z": 70.36}, {"x": 5193.57, "y": 2304.68, "z": 70.33}, {"x": 5208.92, "y": 2329.99, "z": 69.95}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5190.0, "y": 2287.69, "z": 70.64}, {"x": 5200.5, "y": 2304.89, "z": 70.42}, {"x": 5204.25, "y": 2311.16, "z": 70.31}, {"x": 5213.9, "y": 2326.93, "z": 70.07}], "right_lane_mark_type": "NONE", "successors": [38114347, 38114348, 38114390], "predecessors": [38114630], "right_neighbor_id": null, "left_neighbor_id": null}, "38114426": {"id": 38114426, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5205.75, "y": 2399.69, "z": 67.93}, {"x": 5220.0, "y": 2390.12, "z": 68.56}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5203.94, "y": 2397.25, "z": 67.92}, {"x": 5220.0, "y": 2386.47, "z": 68.63}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38114349], "predecessors": [38133156], "right_neighbor_id": 38114433, "left_neighbor_id": 38114432}, "38114427": {"id": 38114427, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5219.69, "y": 2332.21, "z": 70.08}, {"x": 5218.66, "y": 2332.78, "z": 70.01}, {"x": 5218.35, "y": 2332.87, "z": 70.0}, {"x": 5218.02, "y": 2332.89, "z": 69.98}, {"x": 5208.47, "y": 2336.18, "z": 69.85}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5221.28, "y": 2334.87, "z": 69.99}, {"x": 5219.68, "y": 2335.87, "z": 69.9}, {"x": 5211.69, "y": 2337.78, "z": 69.87}, {"x": 5210.95, "y": 2338.03, "z": 69.9}, {"x": 5210.02, "y": 2338.55, "z": 69.89}], "right_lane_mark_type": "NONE", "successors": [38114335], "predecessors": [38114309], "right_neighbor_id": null, "left_neighbor_id": null}, "38114428": {"id": 38114428, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5227.04, "y": 2385.3, "z": 68.87}, {"x": 5228.12, "y": 2384.9, "z": 68.9}, {"x": 5229.17, "y": 2384.73, "z": 68.94}, {"x": 5230.17, "y": 2384.75, "z": 68.96}, {"x": 5231.12, "y": 2384.94, "z": 68.98}, {"x": 5232.02, "y": 2385.28, "z": 68.98}, {"x": 5232.87, "y": 2385.74, "z": 68.96}, {"x": 5233.66, "y": 2386.3, "z": 68.94}, {"x": 5234.4, "y": 2386.93, "z": 68.92}, {"x": 5235.08, "y": 2387.6, "z": 68.91}, {"x": 5235.7, "y": 2388.29, "z": 68.91}, {"x": 5236.25, "y": 2388.98, "z": 68.91}, {"x": 5236.75, "y": 2389.63, "z": 68.94}, {"x": 5237.88, "y": 2391.1, "z": 68.99}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5225.39, "y": 2382.69, "z": 68.88}, {"x": 5225.83, "y": 2382.44, "z": 68.9}, {"x": 5226.15, "y": 2382.24, "z": 68.91}, {"x": 5226.55, "y": 2382.01, "z": 68.93}, {"x": 5227.01, "y": 2381.77, "z": 68.94}, {"x": 5227.55, "y": 2381.53, "z": 68.96}, {"x": 5228.14, "y": 2381.3, "z": 68.98}, {"x": 5228.8, "y": 2381.1, "z": 69.01}, {"x": 5229.53, "y": 2380.95, "z": 69.03}, {"x": 5230.31, "y": 2380.85, "z": 69.05}, {"x": 5231.15, "y": 2380.83, "z": 69.07}, {"x": 5232.05, "y": 2380.89, "z": 69.09}, {"x": 5233.0, "y": 2381.05, "z": 69.11}, {"x": 5234.0, "y": 2381.33, "z": 69.13}, {"x": 5235.05, "y": 2381.74, "z": 69.12}, {"x": 5236.16, "y": 2382.29, "z": 69.12}, {"x": 5237.31, "y": 2383.0, "z": 69.1}, {"x": 5238.5, "y": 2383.88, "z": 69.06}, {"x": 5239.74, "y": 2384.95, "z": 69.07}, {"x": 5241.02, "y": 2386.22, "z": 69.11}, {"x": 5242.29, "y": 2387.75, "z": 69.16}], "right_lane_mark_type": "NONE", "successors": [38114332], "predecessors": [38114349], "right_neighbor_id": null, "left_neighbor_id": null}, "38114432": {"id": 38114432, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5220.0, "y": 2390.12, "z": 68.56}, {"x": 5205.75, "y": 2399.69, "z": 67.93}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5220.0, "y": 2396.9, "z": 68.31}, {"x": 5215.51, "y": 2399.98, "z": 68.12}, {"x": 5209.27, "y": 2404.3, "z": 67.86}], "right_lane_mark_type": "NONE", "successors": [38110982], "predecessors": [38114436], "right_neighbor_id": null, "left_neighbor_id": 38114426}, "38114433": {"id": 38114433, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5203.94, "y": 2397.25, "z": 67.92}, {"x": 5220.0, "y": 2386.47, "z": 68.63}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 5200.63, "y": 2392.74, "z": 67.86}, {"x": 5202.25, "y": 2391.63, "z": 67.93}, {"x": 5220.0, "y": 2379.6, "z": 68.7}], "right_lane_mark_type": "NONE", "successors": [38114404], "predecessors": [38133153], "right_neighbor_id": null, "left_neighbor_id": 38114426}, "38114436": {"id": 38114436, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5227.04, "y": 2385.3, "z": 68.87}, {"x": 5220.0, "y": 2390.12, "z": 68.56}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5230.05, "y": 2390.09, "z": 68.78}, {"x": 5228.92, "y": 2390.89, "z": 68.76}, {"x": 5220.0, "y": 2396.9, "z": 68.31}], "right_lane_mark_type": "NONE", "successors": [38114432], "predecessors": [38114340, 38114318], "right_neighbor_id": null, "left_neighbor_id": 38114349}, "38114446": {"id": 38114446, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5230.74, "y": 2365.5, "z": 69.44}, {"x": 5231.93, "y": 2367.44, "z": 69.35}, {"x": 5232.28, "y": 2367.88, "z": 69.33}, {"x": 5232.93, "y": 2368.58, "z": 69.31}, {"x": 5233.63, "y": 2369.21, "z": 69.3}, {"x": 5234.38, "y": 2369.77, "z": 69.31}, {"x": 5235.18, "y": 2370.27, "z": 69.34}, {"x": 5236.02, "y": 2370.7, "z": 69.36}, {"x": 5236.88, "y": 2371.04, "z": 69.39}, {"x": 5237.77, "y": 2371.3, "z": 69.41}, {"x": 5238.67, "y": 2371.47, "z": 69.44}, {"x": 5239.59, "y": 2371.55, "z": 69.46}, {"x": 5240.51, "y": 2371.53, "z": 69.48}, {"x": 5241.43, "y": 2371.41, "z": 69.51}, {"x": 5242.34, "y": 2371.17, "z": 69.54}, {"x": 5243.24, "y": 2370.83, "z": 69.57}, {"x": 5244.11, "y": 2370.36, "z": 69.6}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5235.67, "y": 2362.4, "z": 69.55}, {"x": 5236.44, "y": 2363.66, "z": 69.51}, {"x": 5236.83, "y": 2364.17, "z": 69.5}, {"x": 5237.2, "y": 2364.59, "z": 69.48}, {"x": 5237.57, "y": 2364.97, "z": 69.46}, {"x": 5238.03, "y": 2365.31, "z": 69.46}, {"x": 5238.44, "y": 2365.5, "z": 69.45}, {"x": 5238.94, "y": 2365.61, "z": 69.46}, {"x": 5239.44, "y": 2365.66, "z": 69.47}, {"x": 5240.0, "y": 2365.59, "z": 69.48}, {"x": 5240.68, "y": 2365.32, "z": 69.5}, {"x": 5240.78, "y": 2365.26, "z": 69.5}], "right_lane_mark_type": "NONE", "successors": [38109359], "predecessors": [38114351], "right_neighbor_id": null, "left_neighbor_id": null}, "38114630": {"id": 38114630, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5159.3, "y": 2248.4, "z": 71.45}, {"x": 5190.0, "y": 2298.9, "z": 70.41}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5164.32, "y": 2245.34, "z": 71.5}, {"x": 5166.74, "y": 2249.34, "z": 71.42}, {"x": 5171.2, "y": 2256.79, "z": 71.25}, {"x": 5180.6, "y": 2272.37, "z": 70.94}, {"x": 5188.46, "y": 2285.2, "z": 70.69}, {"x": 5190.0, "y": 2287.69, "z": 70.64}], "right_lane_mark_type": "NONE", "successors": [38114410], "predecessors": [38114695, 38114661, 38114679], "right_neighbor_id": null, "left_neighbor_id": null}, "38114654": {"id": 38114654, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5269.47, "y": 2420.63, "z": 69.66}, {"x": 5280.0, "y": 2413.56, "z": 70.11}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5266.45, "y": 2416.22, "z": 69.64}, {"x": 5266.77, "y": 2416.01, "z": 69.66}, {"x": 5267.88, "y": 2415.46, "z": 69.69}, {"x": 5280.0, "y": 2407.51, "z": 70.05}], "right_lane_mark_type": "NONE", "successors": [38109262], "predecessors": [38114664, 38114712], "right_neighbor_id": null, "left_neighbor_id": null}, "38114664": {"id": 38114664, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5259.1, "y": 2427.54, "z": 69.6}, {"x": 5268.01, "y": 2421.53, "z": 69.59}, {"x": 5269.47, "y": 2420.63, "z": 69.66}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5256.18, "y": 2423.59, "z": 69.51}, {"x": 5266.45, "y": 2416.22, "z": 69.64}], "right_lane_mark_type": "NONE", "successors": [38114654], "predecessors": [38116264], "right_neighbor_id": null, "left_neighbor_id": null}, "38114672": {"id": 38114672, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5257.14, "y": 2419.46, "z": 69.48}, {"x": 5262.76, "y": 2428.14, "z": 69.58}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5262.48, "y": 2415.83, "z": 69.51}, {"x": 5268.51, "y": 2424.22, "z": 69.62}], "right_lane_mark_type": "NONE", "successors": [38114694], "predecessors": [38109824], "right_neighbor_id": null, "left_neighbor_id": null}, "38114673": {"id": 38114673, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5259.1, "y": 2427.54, "z": 69.6}, {"x": 5260.03, "y": 2426.82, "z": 69.52}, {"x": 5260.36, "y": 2426.74, "z": 69.53}, {"x": 5260.44, "y": 2426.68, "z": 69.53}, {"x": 5260.71, "y": 2426.65, "z": 69.54}, {"x": 5260.87, "y": 2426.62, "z": 69.54}, {"x": 5261.59, "y": 2426.8, "z": 69.55}, {"x": 5262.17, "y": 2427.21, "z": 69.57}, {"x": 5262.57, "y": 2427.7, "z": 69.58}, {"x": 5262.76, "y": 2428.14, "z": 69.58}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5256.18, "y": 2423.59, "z": 69.51}, {"x": 5259.45, "y": 2421.38, "z": 69.52}, {"x": 5260.31, "y": 2421.16, "z": 69.53}, {"x": 5261.23, "y": 2421.02, "z": 69.54}, {"x": 5262.18, "y": 2420.97, "z": 69.53}, {"x": 5263.16, "y": 2421.02, "z": 69.53}, {"x": 5264.14, "y": 2421.19, "z": 69.53}, {"x": 5265.1, "y": 2421.49, "z": 69.53}, {"x": 5266.04, "y": 2421.92, "z": 69.53}, {"x": 5266.93, "y": 2422.52, "z": 69.54}, {"x": 5267.76, "y": 2423.28, "z": 69.6}, {"x": 5268.51, "y": 2424.22, "z": 69.62}], "right_lane_mark_type": "NONE", "successors": [38114694], "predecessors": [38116264], "right_neighbor_id": null, "left_neighbor_id": null}, "38114694": {"id": 38114694, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5262.76, "y": 2428.14, "z": 69.58}, {"x": 5268.61, "y": 2437.19, "z": 69.76}, {"x": 5305.05, "y": 2487.92, "z": 69.94}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5268.51, "y": 2424.22, "z": 69.62}, {"x": 5311.05, "y": 2483.62, "z": 69.91}], "right_lane_mark_type": "NONE", "successors": [38114658, 38114708, 38114688], "predecessors": [38114672, 38114673], "right_neighbor_id": null, "left_neighbor_id": null}, "38114698": {"id": 38114698, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5220.0, "y": 2454.47, "z": 67.99}, {"x": 5222.08, "y": 2453.05, "z": 68.15}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5220.0, "y": 2448.31, "z": 68.12}, {"x": 5220.21, "y": 2448.15, "z": 68.12}], "right_lane_mark_type": "NONE", "successors": [38116233, 38116384], "predecessors": [38119413], "right_neighbor_id": null, "left_neighbor_id": null}, "38114712": {"id": 38114712, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5257.14, "y": 2419.46, "z": 69.48}, {"x": 5257.87, "y": 2420.33, "z": 69.49}, {"x": 5259.35, "y": 2421.67, "z": 69.52}, {"x": 5260.1, "y": 2422.16, "z": 69.53}, {"x": 5260.85, "y": 2422.54, "z": 69.54}, {"x": 5261.61, "y": 2422.81, "z": 69.55}, {"x": 5262.37, "y": 2422.97, "z": 69.56}, {"x": 5263.14, "y": 2423.04, "z": 69.57}, {"x": 5263.91, "y": 2423.01, "z": 69.56}, {"x": 5264.69, "y": 2422.9, "z": 69.55}, {"x": 5265.48, "y": 2422.7, "z": 69.55}, {"x": 5266.26, "y": 2422.42, "z": 69.54}, {"x": 5267.06, "y": 2422.07, "z": 69.53}, {"x": 5267.86, "y": 2421.65, "z": 69.58}, {"x": 5268.66, "y": 2421.17, "z": 69.61}, {"x": 5269.47, "y": 2420.63, "z": 69.66}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5262.48, "y": 2415.83, "z": 69.51}, {"x": 5262.79, "y": 2416.24, "z": 69.52}, {"x": 5263.17, "y": 2416.76, "z": 69.5}, {"x": 5263.51, "y": 2417.09, "z": 69.49}, {"x": 5264.04, "y": 2417.26, "z": 69.5}, {"x": 5264.56, "y": 2417.17, "z": 69.52}, {"x": 5266.45, "y": 2416.22, "z": 69.64}], "right_lane_mark_type": "NONE", "successors": [38114654], "predecessors": [38109824], "right_neighbor_id": null, "left_neighbor_id": null}, "38114770": {"id": 38114770, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5131.81, "y": 2454.6, "z": 64.83}, {"x": 5130.0, "y": 2456.9, "z": 64.75}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5133.92, "y": 2458.31, "z": 64.74}, {"x": 5130.0, "y": 2462.47, "z": 64.59}], "right_lane_mark_type": "SOLID_WHITE", "successors": [38111163], "predecessors": [38111898], "right_neighbor_id": null, "left_neighbor_id": null}, "38115008": {"id": 38115008, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5262.32, "y": 2309.59, "z": 71.59}, {"x": 5262.01, "y": 2309.22, "z": 71.6}, {"x": 5262.0, "y": 2308.87, "z": 71.61}, {"x": 5261.95, "y": 2308.72, "z": 71.61}, {"x": 5261.88, "y": 2308.24, "z": 71.64}, {"x": 5261.92, "y": 2308.15, "z": 71.65}, {"x": 5261.92, "y": 2307.71, "z": 71.66}, {"x": 5262.35, "y": 2307.28, "z": 71.72}, {"x": 5262.37, "y": 2307.24, "z": 71.72}, {"x": 5262.87, "y": 2306.91, "z": 71.77}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5258.49, "y": 2312.06, "z": 71.56}, {"x": 5258.06, "y": 2310.81, "z": 71.57}, {"x": 5258.05, "y": 2309.42, "z": 71.58}, {"x": 5258.4, "y": 2308.01, "z": 71.59}, {"x": 5259.01, "y": 2306.74, "z": 71.61}, {"x": 5259.82, "y": 2305.72, "z": 71.66}, {"x": 5260.49, "y": 2305.28, "z": 71.71}, {"x": 5260.63, "y": 2305.2, "z": 71.72}, {"x": 5261.25, "y": 2304.78, "z": 71.77}], "right_lane_mark_type": "NONE", "successors": [38116016], "predecessors": [38115599], "right_neighbor_id": null, "left_neighbor_id": null}, "38115184": {"id": 38115184, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5302.93, "y": 2400.3, "z": 70.56}, {"x": 5302.29, "y": 2399.4, "z": 70.56}, {"x": 5302.29, "y": 2399.35, "z": 70.56}, {"x": 5302.04, "y": 2398.98, "z": 70.56}, {"x": 5302.02, "y": 2398.79, "z": 70.56}, {"x": 5302.1, "y": 2398.58, "z": 70.57}, {"x": 5302.24, "y": 2398.41, "z": 70.58}, {"x": 5302.76, "y": 2398.06, "z": 70.61}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5299.36, "y": 2402.61, "z": 70.52}, {"x": 5298.71, "y": 2401.7, "z": 70.51}, {"x": 5298.1, "y": 2400.57, "z": 70.47}, {"x": 5297.81, "y": 2399.21, "z": 70.47}, {"x": 5297.82, "y": 2397.73, "z": 70.48}, {"x": 5298.16, "y": 2396.26, "z": 70.5}, {"x": 5298.84, "y": 2394.92, "z": 70.54}, {"x": 5299.9, "y": 2393.9, "z": 70.58}], "right_lane_mark_type": "NONE", "successors": [38116425], "predecessors": [38116473], "right_neighbor_id": null, "left_neighbor_id": null}, "38115208": {"id": 38115208, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5254.01, "y": 2309.47, "z": 71.63}, {"x": 5253.92, "y": 2309.64, "z": 71.63}, {"x": 5252.61, "y": 2311.09, "z": 71.59}, {"x": 5251.54, "y": 2312.09, "z": 71.57}, {"x": 5240.51, "y": 2319.7, "z": 71.41}, {"x": 5224.76, "y": 2328.95, "z": 70.43}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5256.05, "y": 2312.88, "z": 71.56}, {"x": 5226.56, "y": 2331.45, "z": 70.42}], "right_lane_mark_type": "NONE", "successors": [38114309], "predecessors": [38116651, 38116021], "right_neighbor_id": null, "left_neighbor_id": null}, "38115599": {"id": 38115599, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5274.25, "y": 2328.25, "z": 71.24}, {"x": 5268.13, "y": 2319.68, "z": 71.4}, {"x": 5268.02, "y": 2319.49, "z": 71.41}, {"x": 5262.6, "y": 2310.08, "z": 71.58}, {"x": 5262.53, "y": 2309.95, "z": 71.59}, {"x": 5262.43, "y": 2309.77, "z": 71.59}, {"x": 5262.32, "y": 2309.59, "z": 71.59}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5269.43, "y": 2331.55, "z": 71.14}, {"x": 5266.23, "y": 2325.27, "z": 71.34}, {"x": 5266.24, "y": 2325.24, "z": 71.34}, {"x": 5266.02, "y": 2324.61, "z": 71.35}, {"x": 5265.82, "y": 2324.46, "z": 71.35}, {"x": 5265.26, "y": 2323.58, "z": 71.37}, {"x": 5259.45, "y": 2314.46, "z": 71.54}, {"x": 5258.6, "y": 2312.24, "z": 71.56}, {"x": 5258.49, "y": 2312.06, "z": 71.56}], "right_lane_mark_type": "NONE", "successors": [38116375, 38116021, 38115008], "predecessors": [38109482], "right_neighbor_id": null, "left_neighbor_id": null}, "38115671": {"id": 38115671, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5226.56, "y": 2331.45, "z": 70.42}, {"x": 5256.07, "y": 2312.86, "z": 71.56}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5224.76, "y": 2328.95, "z": 70.43}, {"x": 5240.51, "y": 2319.7, "z": 71.41}, {"x": 5251.54, "y": 2312.09, "z": 71.57}, {"x": 5252.61, "y": 2311.09, "z": 71.59}, {"x": 5253.92, "y": 2309.64, "z": 71.63}, {"x": 5254.02, "y": 2309.45, "z": 71.64}], "right_lane_mark_type": "NONE", "successors": [38116338, 38116337], "predecessors": [38114334], "right_neighbor_id": null, "left_neighbor_id": null}, "38116016": {"id": 38116016, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5262.87, "y": 2306.91, "z": 71.77}, {"x": 5296.48, "y": 2284.83, "z": 72.77}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5261.25, "y": 2304.78, "z": 71.77}, {"x": 5294.99, "y": 2282.02, "z": 72.82}], "right_lane_mark_type": "NONE", "successors": [38114327], "predecessors": [38115008, 38116338], "right_neighbor_id": null, "left_neighbor_id": null}, "38116021": {"id": 38116021, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5262.32, "y": 2309.59, "z": 71.59}, {"x": 5261.23, "y": 2308.61, "z": 71.61}, {"x": 5259.78, "y": 2308.04, "z": 71.6}, {"x": 5258.15, "y": 2307.84, "z": 71.6}, {"x": 5256.53, "y": 2307.98, "z": 71.59}, {"x": 5255.19, "y": 2308.51, "z": 71.61}, {"x": 5254.01, "y": 2309.47, "z": 71.63}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5258.49, "y": 2312.06, "z": 71.56}, {"x": 5258.39, "y": 2311.9, "z": 71.56}, {"x": 5257.95, "y": 2311.81, "z": 71.56}, {"x": 5256.98, "y": 2312.29, "z": 71.58}, {"x": 5256.86, "y": 2312.36, "z": 71.57}, {"x": 5256.05, "y": 2312.88, "z": 71.56}], "right_lane_mark_type": "NONE", "successors": [38115208], "predecessors": [38115599], "right_neighbor_id": null, "left_neighbor_id": null}, "38116066": {"id": 38116066, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5089.91, "y": 2422.35, "z": 62.6}, {"x": 5122.49, "y": 2400.0, "z": 64.42}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5088.2, "y": 2420.16, "z": 62.58}, {"x": 5091.22, "y": 2418.13, "z": 62.78}, {"x": 5098.03, "y": 2413.46, "z": 63.16}, {"x": 5117.72, "y": 2400.0, "z": 64.3}], "right_lane_mark_type": "NONE", "successors": [38119874], "predecessors": [38116069], "right_neighbor_id": null, "left_neighbor_id": null}, "38116069": {"id": 38116069, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5082.04, "y": 2421.08, "z": 62.25}, {"x": 5083.05, "y": 2422.38, "z": 62.27}, {"x": 5084.13, "y": 2423.28, "z": 62.3}, {"x": 5085.31, "y": 2423.77, "z": 62.34}, {"x": 5086.57, "y": 2423.85, "z": 62.4}, {"x": 5087.94, "y": 2423.49, "z": 62.47}, {"x": 5089.42, "y": 2422.68, "z": 62.56}, {"x": 5089.91, "y": 2422.35, "z": 62.6}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5084.69, "y": 2420.0, "z": 62.3}, {"x": 5084.98, "y": 2420.46, "z": 62.31}, {"x": 5085.48, "y": 2421.06, "z": 62.34}, {"x": 5085.51, "y": 2421.09, "z": 62.35}, {"x": 5085.83, "y": 2421.18, "z": 62.36}, {"x": 5086.28, "y": 2421.23, "z": 62.4}, {"x": 5086.68, "y": 2421.11, "z": 62.43}, {"x": 5087.1, "y": 2420.93, "z": 62.46}, {"x": 5088.2, "y": 2420.16, "z": 62.58}], "right_lane_mark_type": "NONE", "successors": [38116066], "predecessors": [38116700], "right_neighbor_id": null, "left_neighbor_id": null}, "38116085": {"id": 38116085, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5264.72, "y": 2359.16, "z": 70.25}, {"x": 5245.56, "y": 2372.45, "z": 69.59}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5268.05, "y": 2363.91, "z": 70.19}, {"x": 5259.13, "y": 2369.98, "z": 69.9}, {"x": 5249.26, "y": 2376.85, "z": 69.59}, {"x": 5249.22, "y": 2376.88, "z": 69.58}], "right_lane_mark_type": "NONE", "successors": [38114340, 38114376], "predecessors": [38109382], "right_neighbor_id": null, "left_neighbor_id": null}, "38116233": {"id": 38116233, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5222.08, "y": 2453.05, "z": 68.15}, {"x": 5223.55, "y": 2452.05, "z": 68.17}, {"x": 5228.26, "y": 2448.83, "z": 68.32}, {"x": 5229.11, "y": 2448.25, "z": 68.37}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5220.21, "y": 2448.15, "z": 68.12}, {"x": 5221.88, "y": 2446.94, "z": 68.17}, {"x": 5226.28, "y": 2443.74, "z": 68.37}], "right_lane_mark_type": "NONE", "successors": [38116264], "predecessors": [38114698], "right_neighbor_id": null, "left_neighbor_id": null}, "38116264": {"id": 38116264, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5229.11, "y": 2448.25, "z": 68.37}, {"x": 5259.1, "y": 2427.54, "z": 69.6}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5226.28, "y": 2443.74, "z": 68.37}, {"x": 5256.18, "y": 2423.59, "z": 69.51}], "right_lane_mark_type": "NONE", "successors": [38114664, 38114673], "predecessors": [38116233, 38116298], "right_neighbor_id": null, "left_neighbor_id": null}, "38116297": {"id": 38116297, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5225.26, "y": 2453.64, "z": 68.28}, {"x": 5228.43, "y": 2458.41, "z": 68.39}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5229.14, "y": 2450.93, "z": 68.32}, {"x": 5232.62, "y": 2455.82, "z": 68.4}], "right_lane_mark_type": "NONE", "successors": [], "predecessors": [38116384], "right_neighbor_id": null, "left_neighbor_id": null}, "38116298": {"id": 38116298, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5229.15, "y": 2450.95, "z": 68.32}, {"x": 5228.19, "y": 2449.59, "z": 68.32}, {"x": 5228.15, "y": 2449.46, "z": 68.32}, {"x": 5228.05, "y": 2449.31, "z": 68.32}, {"x": 5228.1, "y": 2449.17, "z": 68.32}, {"x": 5228.11, "y": 2449.03, "z": 68.32}, {"x": 5228.19, "y": 2448.92, "z": 68.32}, {"x": 5228.24, "y": 2448.79, "z": 68.32}, {"x": 5229.11, "y": 2448.25, "z": 68.37}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5225.28, "y": 2453.66, "z": 68.28}, {"x": 5224.37, "y": 2452.3, "z": 68.21}, {"x": 5224.07, "y": 2452.09, "z": 68.19}, {"x": 5223.79, "y": 2451.67, "z": 68.18}, {"x": 5223.28, "y": 2450.4, "z": 68.16}, {"x": 5223.04, "y": 2448.86, "z": 68.16}, {"x": 5223.31, "y": 2447.15, "z": 68.2}, {"x": 5224.32, "y": 2445.41, "z": 68.28}, {"x": 5226.28, "y": 2443.74, "z": 68.37}], "right_lane_mark_type": "NONE", "successors": [38116264], "predecessors": [38116484], "right_neighbor_id": null, "left_neighbor_id": null}, "38116337": {"id": 38116337, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5256.07, "y": 2312.86, "z": 71.56}, {"x": 5256.73, "y": 2312.22, "z": 71.58}, {"x": 5258.42, "y": 2309.94, "z": 71.57}, {"x": 5258.92, "y": 2308.63, "z": 71.58}, {"x": 5259.06, "y": 2307.2, "z": 71.61}, {"x": 5258.72, "y": 2305.64, "z": 71.63}, {"x": 5257.8, "y": 2303.94, "z": 71.65}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5254.02, "y": 2309.45, "z": 71.64}, {"x": 5254.46, "y": 2308.77, "z": 71.63}, {"x": 5254.89, "y": 2307.87, "z": 71.61}, {"x": 5254.87, "y": 2307.16, "z": 71.61}, {"x": 5254.42, "y": 2306.47, "z": 71.63}], "right_lane_mark_type": "NONE", "successors": [38116470], "predecessors": [38115671], "right_neighbor_id": null, "left_neighbor_id": null}, "38116338": {"id": 38116338, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5256.07, "y": 2312.86, "z": 71.56}, {"x": 5261.92, "y": 2307.74, "z": 71.66}, {"x": 5261.92, "y": 2307.71, "z": 71.66}, {"x": 5262.4, "y": 2307.24, "z": 71.72}, {"x": 5262.87, "y": 2306.91, "z": 71.77}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5254.02, "y": 2309.45, "z": 71.64}, {"x": 5260.31, "y": 2305.39, "z": 71.7}, {"x": 5260.63, "y": 2305.2, "z": 71.72}, {"x": 5261.25, "y": 2304.78, "z": 71.77}], "right_lane_mark_type": "NONE", "successors": [38116016], "predecessors": [38115671], "right_neighbor_id": null, "left_neighbor_id": null}, "38116340": {"id": 38116340, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5250.0, "y": 2294.7, "z": 71.75}, {"x": 5242.61, "y": 2282.67, "z": 71.96}, {"x": 5240.13, "y": 2278.14, "z": 72.02}, {"x": 5234.68, "y": 2268.27, "z": 72.13}, {"x": 5226.43, "y": 2250.0, "z": 72.42}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5250.0, "y": 2299.5, "z": 71.78}, {"x": 5221.73, "y": 2252.38, "z": 72.43}, {"x": 5220.31, "y": 2250.0, "z": 72.46}], "right_lane_mark_type": "NONE", "successors": [38117885], "predecessors": [38116470], "right_neighbor_id": null, "left_neighbor_id": null}, "38116375": {"id": 38116375, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5262.32, "y": 2309.59, "z": 71.59}, {"x": 5258.14, "y": 2304.35, "z": 71.64}, {"x": 5257.8, "y": 2303.94, "z": 71.65}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5258.49, "y": 2312.06, "z": 71.56}, {"x": 5258.39, "y": 2311.9, "z": 71.56}, {"x": 5254.87, "y": 2307.16, "z": 71.61}, {"x": 5254.42, "y": 2306.47, "z": 71.63}], "right_lane_mark_type": "NONE", "successors": [38116470], "predecessors": [38115599], "right_neighbor_id": null, "left_neighbor_id": null}, "38116378": {"id": 38116378, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5294.97, "y": 2281.98, "z": 72.82}, {"x": 5261.25, "y": 2304.78, "z": 71.77}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5296.48, "y": 2284.83, "z": 72.77}, {"x": 5262.87, "y": 2306.91, "z": 71.77}], "right_lane_mark_type": "NONE", "successors": [38116651, 38116650], "predecessors": [38114310], "right_neighbor_id": null, "left_neighbor_id": null}, "38116384": {"id": 38116384, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5222.08, "y": 2453.05, "z": 68.15}, {"x": 5223.55, "y": 2452.05, "z": 68.17}, {"x": 5223.78, "y": 2452.02, "z": 68.19}, {"x": 5224.07, "y": 2452.09, "z": 68.19}, {"x": 5224.37, "y": 2452.3, "z": 68.21}, {"x": 5225.26, "y": 2453.64, "z": 68.28}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5220.21, "y": 2448.15, "z": 68.12}, {"x": 5220.68, "y": 2447.92, "z": 68.13}, {"x": 5221.75, "y": 2447.5, "z": 68.16}, {"x": 5223.26, "y": 2447.22, "z": 68.19}, {"x": 5225.03, "y": 2447.38, "z": 68.25}, {"x": 5226.9, "y": 2448.29, "z": 68.29}, {"x": 5228.61, "y": 2450.19, "z": 68.32}, {"x": 5229.14, "y": 2450.93, "z": 68.32}], "right_lane_mark_type": "NONE", "successors": [38116297], "predecessors": [38114698], "right_neighbor_id": null, "left_neighbor_id": null}, "38116425": {"id": 38116425, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5302.76, "y": 2398.06, "z": 70.61}, {"x": 5313.26, "y": 2391.01, "z": 71.07}, {"x": 5313.95, "y": 2390.16, "z": 71.06}, {"x": 5314.42, "y": 2389.98, "z": 71.08}, {"x": 5315.03, "y": 2389.93, "z": 71.11}, {"x": 5337.84, "y": 2373.86, "z": 71.66}, {"x": 5338.4, "y": 2373.55, "z": 71.66}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5299.9, "y": 2393.9, "z": 70.58}, {"x": 5335.67, "y": 2368.8, "z": 71.67}, {"x": 5335.75, "y": 2368.73, "z": 71.67}, {"x": 5335.93, "y": 2368.61, "z": 71.67}], "right_lane_mark_type": "NONE", "successors": [38109421, 38109292], "predecessors": [38115184, 38116557], "right_neighbor_id": null, "left_neighbor_id": null}, "38116470": {"id": 38116470, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5257.8, "y": 2303.94, "z": 71.65}, {"x": 5250.0, "y": 2294.7, "z": 71.75}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5254.42, "y": 2306.47, "z": 71.63}, {"x": 5250.0, "y": 2299.5, "z": 71.78}], "right_lane_mark_type": "NONE", "successors": [38116340], "predecessors": [38116337, 38116650, 38116375], "right_neighbor_id": null, "left_neighbor_id": null}, "38116473": {"id": 38116473, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5316.7, "y": 2419.79, "z": 70.48}, {"x": 5302.93, "y": 2400.3, "z": 70.56}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5311.88, "y": 2419.73, "z": 70.46}, {"x": 5299.36, "y": 2402.61, "z": 70.52}], "right_lane_mark_type": "NONE", "successors": [38115184], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "38116484": {"id": 38116484, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5232.62, "y": 2455.82, "z": 68.4}, {"x": 5229.15, "y": 2450.95, "z": 68.32}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5228.43, "y": 2458.41, "z": 68.39}, {"x": 5225.28, "y": 2453.66, "z": 68.28}], "right_lane_mark_type": "NONE", "successors": [38116298], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "38116487": {"id": 38116487, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5296.66, "y": 2402.37, "z": 70.48}, {"x": 5296.78, "y": 2402.29, "z": 70.49}, {"x": 5297.38, "y": 2401.89, "z": 70.53}, {"x": 5297.66, "y": 2401.71, "z": 70.53}, {"x": 5297.98, "y": 2401.68, "z": 70.55}, {"x": 5298.33, "y": 2401.69, "z": 70.53}, {"x": 5298.65, "y": 2401.74, "z": 70.51}, {"x": 5299.22, "y": 2402.42, "z": 70.52}, {"x": 5299.36, "y": 2402.61, "z": 70.52}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5293.79, "y": 2398.19, "z": 70.4}, {"x": 5294.68, "y": 2397.65, "z": 70.43}, {"x": 5295.89, "y": 2397.35, "z": 70.44}, {"x": 5297.28, "y": 2397.28, "z": 70.47}, {"x": 5298.77, "y": 2397.48, "z": 70.5}, {"x": 5300.26, "y": 2398.03, "z": 70.53}, {"x": 5301.69, "y": 2398.96, "z": 70.56}, {"x": 5302.96, "y": 2400.33, "z": 70.55}], "right_lane_mark_type": "NONE", "successors": [38116606], "predecessors": [38109262], "right_neighbor_id": null, "left_neighbor_id": null}, "38116534": {"id": 38116534, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5089.09, "y": 2421.29, "z": 62.55}, {"x": 5087.99, "y": 2422.05, "z": 62.48}, {"x": 5086.13, "y": 2423.01, "z": 62.38}, {"x": 5084.72, "y": 2423.19, "z": 62.32}, {"x": 5083.64, "y": 2422.8, "z": 62.28}, {"x": 5082.79, "y": 2422.03, "z": 62.26}, {"x": 5082.04, "y": 2421.08, "z": 62.25}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5090.79, "y": 2423.51, "z": 62.58}, {"x": 5089.6, "y": 2424.32, "z": 62.52}, {"x": 5087.83, "y": 2425.34, "z": 62.47}, {"x": 5086.18, "y": 2425.91, "z": 62.35}, {"x": 5084.7, "y": 2426.03, "z": 62.29}, {"x": 5083.37, "y": 2425.77, "z": 62.24}, {"x": 5082.19, "y": 2425.18, "z": 62.21}, {"x": 5081.13, "y": 2424.32, "z": 62.18}, {"x": 5080.16, "y": 2423.25, "z": 62.14}, {"x": 5079.28, "y": 2422.02, "z": 62.09}], "right_lane_mark_type": "NONE", "successors": [38116936], "predecessors": [38116727], "right_neighbor_id": null, "left_neighbor_id": null}, "38116557": {"id": 38116557, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5296.66, "y": 2402.37, "z": 70.48}, {"x": 5302.19, "y": 2398.47, "z": 70.58}, {"x": 5302.24, "y": 2398.41, "z": 70.58}, {"x": 5302.76, "y": 2398.06, "z": 70.61}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5293.79, "y": 2398.19, "z": 70.4}, {"x": 5299.9, "y": 2393.9, "z": 70.58}], "right_lane_mark_type": "NONE", "successors": [38116425], "predecessors": [38109262], "right_neighbor_id": null, "left_neighbor_id": null}, "38116606": {"id": 38116606, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5299.36, "y": 2402.61, "z": 70.52}, {"x": 5311.88, "y": 2419.73, "z": 70.46}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5302.96, "y": 2400.33, "z": 70.55}, {"x": 5316.7, "y": 2419.79, "z": 70.48}], "right_lane_mark_type": "NONE", "successors": [], "predecessors": [38116487], "right_neighbor_id": null, "left_neighbor_id": null}, "38116650": {"id": 38116650, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5261.25, "y": 2304.78, "z": 71.77}, {"x": 5260.36, "y": 2305.49, "z": 71.7}, {"x": 5259.68, "y": 2305.73, "z": 71.66}, {"x": 5258.92, "y": 2305.29, "z": 71.64}, {"x": 5257.91, "y": 2304.07, "z": 71.64}, {"x": 5257.8, "y": 2303.94, "z": 71.65}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5262.87, "y": 2306.91, "z": 71.77}, {"x": 5259.96, "y": 2308.24, "z": 71.6}, {"x": 5258.52, "y": 2308.39, "z": 71.59}, {"x": 5257.11, "y": 2308.21, "z": 71.59}, {"x": 5255.74, "y": 2307.57, "z": 71.6}, {"x": 5254.42, "y": 2306.47, "z": 71.63}], "right_lane_mark_type": "NONE", "successors": [38116470], "predecessors": [38116378], "right_neighbor_id": null, "left_neighbor_id": null}, "38116651": {"id": 38116651, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5261.25, "y": 2304.78, "z": 71.77}, {"x": 5260.63, "y": 2305.2, "z": 71.72}, {"x": 5260.34, "y": 2305.37, "z": 71.7}, {"x": 5254.01, "y": 2309.47, "z": 71.63}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5262.87, "y": 2306.91, "z": 71.77}, {"x": 5262.4, "y": 2307.24, "z": 71.72}, {"x": 5261.92, "y": 2307.71, "z": 71.66}, {"x": 5261.92, "y": 2307.74, "z": 71.66}, {"x": 5256.05, "y": 2312.88, "z": 71.56}], "right_lane_mark_type": "NONE", "successors": [38115208], "predecessors": [38116378], "right_neighbor_id": null, "left_neighbor_id": null}, "38116700": {"id": 38116700, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5045.15, "y": 2364.79, "z": 60.56}, {"x": 5082.04, "y": 2421.08, "z": 62.25}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5047.63, "y": 2363.08, "z": 60.59}, {"x": 5051.0, "y": 2368.54, "z": 60.62}, {"x": 5055.49, "y": 2375.57, "z": 60.85}, {"x": 5056.23, "y": 2376.61, "z": 60.91}, {"x": 5058.09, "y": 2379.47, "z": 61.07}, {"x": 5058.92, "y": 2380.54, "z": 61.13}, {"x": 5061.01, "y": 2383.65, "z": 61.26}, {"x": 5074.62, "y": 2404.43, "z": 62.03}, {"x": 5084.69, "y": 2420.0, "z": 62.3}], "right_lane_mark_type": "NONE", "successors": [38116069], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 38116936}, "38116727": {"id": 38116727, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5120.0, "y": 2400.0, "z": 64.35}, {"x": 5089.09, "y": 2421.29, "z": 62.55}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5125.13, "y": 2400.0, "z": 64.58}, {"x": 5090.79, "y": 2423.51, "z": 62.58}], "right_lane_mark_type": "NONE", "successors": [38116534], "predecessors": [38119598], "right_neighbor_id": null, "left_neighbor_id": null}, "38116936": {"id": 38116936, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5082.04, "y": 2421.08, "z": 62.25}, {"x": 5045.15, "y": 2364.79, "z": 60.56}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5079.28, "y": 2422.02, "z": 62.09}, {"x": 5064.28, "y": 2398.88, "z": 61.68}, {"x": 5057.12, "y": 2388.01, "z": 61.24}, {"x": 5055.79, "y": 2386.62, "z": 61.16}, {"x": 5055.18, "y": 2385.75, "z": 61.11}, {"x": 5045.34, "y": 2370.96, "z": 60.58}, {"x": 5042.53, "y": 2366.6, "z": 60.48}], "right_lane_mark_type": "NONE", "successors": [], "predecessors": [38116534], "right_neighbor_id": null, "left_neighbor_id": 38116700}, "38117100": {"id": 38117100, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5264.72, "y": 2359.16, "z": 70.25}, {"x": 5272.94, "y": 2353.69, "z": 70.51}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5260.02, "y": 2352.14, "z": 70.15}, {"x": 5268.73, "y": 2346.16, "z": 70.46}], "right_lane_mark_type": "NONE", "successors": [38109440, 38111858, 38109167], "predecessors": [38109359], "right_neighbor_id": null, "left_neighbor_id": 38109382}, "38117202": {"id": 38117202, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5220.63, "y": 2516.66, "z": 67.47}, {"x": 5213.07, "y": 2506.15, "z": 67.19}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5214.32, "y": 2521.21, "z": 67.33}, {"x": 5206.75, "y": 2510.71, "z": 67.09}], "right_lane_mark_type": "NONE", "successors": [38117242], "predecessors": [38117142], "right_neighbor_id": null, "left_neighbor_id": null}, "38117242": {"id": 38117242, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5213.07, "y": 2506.15, "z": 67.19}, {"x": 5201.69, "y": 2490.0, "z": 66.87}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5206.75, "y": 2510.71, "z": 67.09}, {"x": 5191.84, "y": 2490.0, "z": 66.62}], "right_lane_mark_type": "NONE", "successors": [38111376], "predecessors": [38117202, 38117303], "right_neighbor_id": null, "left_neighbor_id": null}, "38117303": {"id": 38117303, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5208.5, "y": 2516.78, "z": 67.22}, {"x": 5209.49, "y": 2515.8, "z": 67.24}, {"x": 5210.56, "y": 2514.69, "z": 67.27}, {"x": 5211.55, "y": 2513.4, "z": 67.31}, {"x": 5212.42, "y": 2511.99, "z": 67.33}, {"x": 5213.07, "y": 2510.51, "z": 67.29}, {"x": 5213.45, "y": 2509.01, "z": 67.24}, {"x": 5213.47, "y": 2507.54, "z": 67.22}, {"x": 5213.07, "y": 2506.15, "z": 67.19}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5206.09, "y": 2513.97, "z": 67.11}, {"x": 5207.08, "y": 2512.98, "z": 67.11}, {"x": 5207.58, "y": 2512.28, "z": 67.13}, {"x": 5207.38, "y": 2511.52, "z": 67.11}, {"x": 5206.75, "y": 2510.71, "z": 67.09}], "right_lane_mark_type": "NONE", "successors": [38117242], "predecessors": [38117102], "right_neighbor_id": null, "left_neighbor_id": null}, "38118150": {"id": 38118150, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5121.31, "y": 2370.0, "z": 64.41}, {"x": 5111.53, "y": 2354.08, "z": 64.06}, {"x": 5094.14, "y": 2325.55, "z": 63.5}, {"x": 5086.44, "y": 2312.85, "z": 63.25}, {"x": 5077.66, "y": 2298.28, "z": 62.96}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5111.89, "y": 2370.0, "z": 64.14}, {"x": 5070.82, "y": 2302.45, "z": 62.85}], "right_lane_mark_type": "NONE", "successors": [38118245, 38117835], "predecessors": [38119412], "right_neighbor_id": null, "left_neighbor_id": null}, "38118957": {"id": 38118957, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5137.17, "y": 2396.17, "z": 65.11}, {"x": 5132.79, "y": 2388.94, "z": 64.96}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5130.4, "y": 2400.29, "z": 64.88}, {"x": 5125.94, "y": 2392.89, "z": 64.68}, {"x": 5125.81, "y": 2392.68, "z": 64.69}], "right_lane_mark_type": "NONE", "successors": [38119753, 38119931], "predecessors": [38111342], "right_neighbor_id": null, "left_neighbor_id": null}, "38119080": {"id": 38119080, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5125.79, "y": 2397.91, "z": 64.63}, {"x": 5129.19, "y": 2396.33, "z": 64.79}, {"x": 5131.45, "y": 2394.78, "z": 64.91}, {"x": 5132.78, "y": 2393.31, "z": 64.95}, {"x": 5133.38, "y": 2391.97, "z": 64.95}, {"x": 5133.48, "y": 2390.81, "z": 64.94}, {"x": 5133.27, "y": 2389.89, "z": 64.93}, {"x": 5132.97, "y": 2389.25, "z": 64.96}, {"x": 5132.79, "y": 2388.94, "z": 64.96}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5124.28, "y": 2395.59, "z": 64.66}, {"x": 5125.22, "y": 2395.0, "z": 64.67}, {"x": 5125.33, "y": 2394.87, "z": 64.68}, {"x": 5125.67, "y": 2394.65, "z": 64.68}, {"x": 5125.88, "y": 2394.43, "z": 64.68}, {"x": 5126.01, "y": 2394.18, "z": 64.67}, {"x": 5126.03, "y": 2394.11, "z": 64.67}, {"x": 5126.04, "y": 2394.1, "z": 64.67}, {"x": 5126.04, "y": 2394.07, "z": 64.67}, {"x": 5126.07, "y": 2393.94, "z": 64.67}, {"x": 5126.09, "y": 2393.71, "z": 64.68}, {"x": 5126.06, "y": 2393.27, "z": 64.68}, {"x": 5125.93, "y": 2392.88, "z": 64.68}, {"x": 5125.81, "y": 2392.68, "z": 64.69}], "right_lane_mark_type": "NONE", "successors": [38119753, 38119931], "predecessors": [38119874], "right_neighbor_id": null, "left_neighbor_id": null}, "38119412": {"id": 38119412, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5127.59, "y": 2380.33, "z": 64.71}, {"x": 5121.31, "y": 2370.0, "z": 64.41}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5120.8, "y": 2384.53, "z": 64.52}, {"x": 5111.89, "y": 2370.0, "z": 64.14}], "right_lane_mark_type": "NONE", "successors": [38118150], "predecessors": [38119960, 38119753], "right_neighbor_id": null, "left_neighbor_id": null}, "38119413": {"id": 38119413, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5193.29, "y": 2472.78, "z": 66.61}, {"x": 5193.35, "y": 2472.73, "z": 66.62}, {"x": 5220.0, "y": 2454.47, "z": 67.99}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5189.42, "y": 2468.25, "z": 66.55}, {"x": 5193.11, "y": 2465.75, "z": 66.8}, {"x": 5194.56, "y": 2465.23, "z": 66.83}, {"x": 5195.83, "y": 2464.77, "z": 66.86}, {"x": 5220.0, "y": 2448.31, "z": 68.12}], "right_lane_mark_type": "NONE", "successors": [38114698], "predecessors": [38119952], "right_neighbor_id": null, "left_neighbor_id": null}, "38119598": {"id": 38119598, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5125.02, "y": 2396.73, "z": 64.63}, {"x": 5120.0, "y": 2400.0, "z": 64.35}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5126.51, "y": 2399.05, "z": 64.68}, {"x": 5126.28, "y": 2399.21, "z": 64.66}, {"x": 5126.19, "y": 2399.27, "z": 64.66}, {"x": 5125.13, "y": 2400.0, "z": 64.58}], "right_lane_mark_type": "NONE", "successors": [38116727], "predecessors": [38119599], "right_neighbor_id": null, "left_neighbor_id": null}, "38119599": {"id": 38119599, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5137.17, "y": 2396.17, "z": 65.11}, {"x": 5135.95, "y": 2394.66, "z": 65.05}, {"x": 5134.48, "y": 2393.81, "z": 64.99}, {"x": 5132.85, "y": 2393.51, "z": 64.96}, {"x": 5131.13, "y": 2393.67, "z": 64.9}, {"x": 5129.4, "y": 2394.17, "z": 64.82}, {"x": 5127.76, "y": 2394.92, "z": 64.73}, {"x": 5126.27, "y": 2395.8, "z": 64.69}, {"x": 5125.02, "y": 2396.73, "z": 64.63}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5130.4, "y": 2400.29, "z": 64.88}, {"x": 5130.01, "y": 2399.8, "z": 64.84}, {"x": 5129.91, "y": 2399.71, "z": 64.84}, {"x": 5129.76, "y": 2399.54, "z": 64.83}, {"x": 5129.29, "y": 2399.14, "z": 64.79}, {"x": 5128.85, "y": 2398.82, "z": 64.77}, {"x": 5128.25, "y": 2398.63, "z": 64.74}, {"x": 5127.99, "y": 2398.62, "z": 64.74}, {"x": 5127.74, "y": 2398.67, "z": 64.73}, {"x": 5127.16, "y": 2398.75, "z": 64.72}, {"x": 5126.69, "y": 2398.93, "z": 64.71}, {"x": 5126.51, "y": 2399.05, "z": 64.68}], "right_lane_mark_type": "NONE", "successors": [38119598], "predecessors": [38111342], "right_neighbor_id": null, "left_neighbor_id": null}, "38119753": {"id": 38119753, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5132.79, "y": 2388.94, "z": 64.96}, {"x": 5127.59, "y": 2380.33, "z": 64.71}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5125.81, "y": 2392.68, "z": 64.69}, {"x": 5120.8, "y": 2384.53, "z": 64.52}], "right_lane_mark_type": "NONE", "successors": [38119412], "predecessors": [38119080, 38118957], "right_neighbor_id": null, "left_neighbor_id": null}, "38119868": {"id": 38119868, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5133.2, "y": 2385.48, "z": 64.88}, {"x": 5133.24, "y": 2385.46, "z": 64.88}, {"x": 5133.26, "y": 2385.44, "z": 64.88}, {"x": 5154.86, "y": 2370.0, "z": 66.34}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5130.64, "y": 2382.18, "z": 64.81}, {"x": 5131.19, "y": 2381.85, "z": 64.82}, {"x": 5131.38, "y": 2381.7, "z": 64.85}, {"x": 5132.32, "y": 2381.06, "z": 64.91}, {"x": 5132.78, "y": 2380.51, "z": 64.97}, {"x": 5135.8, "y": 2377.72, "z": 65.2}, {"x": 5148.63, "y": 2370.0, "z": 66.08}], "right_lane_mark_type": "NONE", "successors": [38114336], "predecessors": [38119931], "right_neighbor_id": null, "left_neighbor_id": null}, "38119874": {"id": 38119874, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5122.49, "y": 2400.0, "z": 64.42}, {"x": 5125.79, "y": 2397.91, "z": 64.63}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5117.72, "y": 2400.0, "z": 64.3}, {"x": 5121.38, "y": 2397.54, "z": 64.51}, {"x": 5123.18, "y": 2396.32, "z": 64.61}, {"x": 5123.4, "y": 2396.17, "z": 64.62}, {"x": 5124.28, "y": 2395.59, "z": 64.66}], "right_lane_mark_type": "NONE", "successors": [38119080], "predecessors": [38116066], "right_neighbor_id": null, "left_neighbor_id": null}, "38119931": {"id": 38119931, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5132.79, "y": 2388.94, "z": 64.96}, {"x": 5132.68, "y": 2388.55, "z": 64.94}, {"x": 5132.59, "y": 2388.35, "z": 64.93}, {"x": 5132.4, "y": 2387.73, "z": 64.89}, {"x": 5132.37, "y": 2387.41, "z": 64.88}, {"x": 5132.35, "y": 2387.33, "z": 64.88}, {"x": 5132.36, "y": 2387.24, "z": 64.88}, {"x": 5132.35, "y": 2386.79, "z": 64.86}, {"x": 5132.51, "y": 2386.28, "z": 64.86}, {"x": 5132.61, "y": 2386.14, "z": 64.86}, {"x": 5132.81, "y": 2385.75, "z": 64.86}, {"x": 5133.2, "y": 2385.48, "z": 64.88}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5125.81, "y": 2392.68, "z": 64.69}, {"x": 5125.57, "y": 2390.93, "z": 64.68}, {"x": 5125.59, "y": 2389.0, "z": 64.69}, {"x": 5126.01, "y": 2387.27, "z": 64.72}, {"x": 5126.72, "y": 2385.77, "z": 64.75}, {"x": 5127.59, "y": 2384.52, "z": 64.75}, {"x": 5128.52, "y": 2383.56, "z": 64.75}, {"x": 5130.11, "y": 2382.57, "z": 64.8}, {"x": 5130.64, "y": 2382.18, "z": 64.81}], "right_lane_mark_type": "NONE", "successors": [38119868], "predecessors": [38119080, 38118957], "right_neighbor_id": null, "left_neighbor_id": null}, "38119950": {"id": 38119950, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5151.93, "y": 2370.0, "z": 66.2}, {"x": 5131.05, "y": 2382.68, "z": 64.8}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5160.02, "y": 2370.0, "z": 66.66}, {"x": 5150.18, "y": 2376.35, "z": 66.08}, {"x": 5148.08, "y": 2377.2, "z": 65.95}, {"x": 5143.52, "y": 2379.06, "z": 65.53}, {"x": 5133.78, "y": 2385.1, "z": 64.92}, {"x": 5133.18, "y": 2385.48, "z": 64.87}], "right_lane_mark_type": "NONE", "successors": [38119960], "predecessors": [38120641], "right_neighbor_id": null, "left_neighbor_id": null}, "38119951": {"id": 38119951, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5192.71, "y": 2477.54, "z": 66.59}, {"x": 5191.41, "y": 2475.75, "z": 66.57}, {"x": 5191.22, "y": 2475.4, "z": 66.56}, {"x": 5186.94, "y": 2469.42, "z": 66.48}, {"x": 5186.86, "y": 2469.41, "z": 66.47}, {"x": 5186.63, "y": 2469.25, "z": 66.47}, {"x": 5185.06, "y": 2467.04, "z": 66.43}, {"x": 5183.61, "y": 2464.83, "z": 66.42}, {"x": 5169.09, "y": 2444.5, "z": 65.98}, {"x": 5166.37, "y": 2440.79, "z": 65.95}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5186.24, "y": 2482.2, "z": 66.46}, {"x": 5176.6, "y": 2468.77, "z": 66.23}, {"x": 5172.99, "y": 2463.63, "z": 66.18}, {"x": 5166.14, "y": 2454.19, "z": 65.95}, {"x": 5160.14, "y": 2445.81, "z": 65.74}], "right_lane_mark_type": "NONE", "successors": [38111879, 38111884, 38111310, 38111873], "predecessors": [38111376], "right_neighbor_id": null, "left_neighbor_id": null}, "38119952": {"id": 38119952, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5192.71, "y": 2477.54, "z": 66.59}, {"x": 5192.04, "y": 2476.75, "z": 66.57}, {"x": 5191.46, "y": 2475.96, "z": 66.57}, {"x": 5191.42, "y": 2475.77, "z": 66.57}, {"x": 5191.41, "y": 2475.75, "z": 66.57}, {"x": 5191.17, "y": 2475.29, "z": 66.55}, {"x": 5191.11, "y": 2474.94, "z": 66.54}, {"x": 5191.22, "y": 2474.57, "z": 66.55}, {"x": 5191.51, "y": 2474.24, "z": 66.57}, {"x": 5192.45, "y": 2473.5, "z": 66.59}, {"x": 5193.29, "y": 2472.78, "z": 66.61}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5186.24, "y": 2482.2, "z": 66.46}, {"x": 5185.14, "y": 2480.79, "z": 66.43}, {"x": 5184.44, "y": 2479.28, "z": 66.39}, {"x": 5184.1, "y": 2477.72, "z": 66.38}, {"x": 5184.1, "y": 2476.13, "z": 66.42}, {"x": 5184.4, "y": 2474.56, "z": 66.47}, {"x": 5184.98, "y": 2473.04, "z": 66.5}, {"x": 5185.79, "y": 2471.61, "z": 66.48}, {"x": 5186.83, "y": 2470.31, "z": 66.46}, {"x": 5188.04, "y": 2469.18, "z": 66.5}, {"x": 5188.65, "y": 2468.77, "z": 66.52}, {"x": 5188.68, "y": 2468.75, "z": 66.52}, {"x": 5189.42, "y": 2468.25, "z": 66.55}], "right_lane_mark_type": "NONE", "successors": [38119413], "predecessors": [38111376], "right_neighbor_id": null, "left_neighbor_id": null}, "38119960": {"id": 38119960, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5131.05, "y": 2382.68, "z": 64.8}, {"x": 5129.63, "y": 2382.82, "z": 64.78}, {"x": 5129.05, "y": 2382.5, "z": 64.78}, {"x": 5128.73, "y": 2382.21, "z": 64.77}, {"x": 5128.28, "y": 2381.47, "z": 64.74}, {"x": 5127.59, "y": 2380.33, "z": 64.71}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5133.18, "y": 2385.48, "z": 64.87}, {"x": 5131.28, "y": 2386.73, "z": 64.83}, {"x": 5129.54, "y": 2387.52, "z": 64.82}, {"x": 5127.95, "y": 2387.9, "z": 64.8}, {"x": 5126.5, "y": 2387.92, "z": 64.74}, {"x": 5125.17, "y": 2387.64, "z": 64.68}, {"x": 5123.95, "y": 2387.11, "z": 64.62}, {"x": 5122.83, "y": 2386.38, "z": 64.58}, {"x": 5121.78, "y": 2385.5, "z": 64.54}, {"x": 5120.8, "y": 2384.53, "z": 64.52}], "right_lane_mark_type": "NONE", "successors": [38119412], "predecessors": [38119950], "right_neighbor_id": null, "left_neighbor_id": null}, "38119984": {"id": 38119984, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5174.16, "y": 2356.73, "z": 67.63}, {"x": 5173.1, "y": 2356.86, "z": 67.57}, {"x": 5171.73, "y": 2356.5, "z": 67.5}, {"x": 5171.46, "y": 2356.26, "z": 67.49}, {"x": 5171.02, "y": 2356.14, "z": 67.47}, {"x": 5170.73, "y": 2355.66, "z": 67.47}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5175.92, "y": 2359.61, "z": 67.7}, {"x": 5174.59, "y": 2360.44, "z": 67.65}, {"x": 5173.11, "y": 2360.56, "z": 67.54}, {"x": 5171.5, "y": 2360.31, "z": 67.41}, {"x": 5169.97, "y": 2359.71, "z": 67.33}, {"x": 5168.64, "y": 2358.81, "z": 67.3}, {"x": 5167.63, "y": 2357.63, "z": 67.27}], "right_lane_mark_type": "NONE", "successors": [38120280], "predecessors": [38114335], "right_neighbor_id": null, "left_neighbor_id": null}, "38120026": {"id": 38120026, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5167.65, "y": 2357.66, "z": 67.27}, {"x": 5167.82, "y": 2357.96, "z": 67.29}, {"x": 5167.72, "y": 2358.69, "z": 67.27}, {"x": 5167.67, "y": 2358.72, "z": 67.26}, {"x": 5167.65, "y": 2359.86, "z": 67.23}, {"x": 5166.88, "y": 2361.07, "z": 67.17}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5170.71, "y": 2355.63, "z": 67.47}, {"x": 5171.38, "y": 2357.24, "z": 67.45}, {"x": 5171.6, "y": 2358.93, "z": 67.42}, {"x": 5171.42, "y": 2360.58, "z": 67.4}, {"x": 5170.88, "y": 2362.11, "z": 67.38}, {"x": 5170.03, "y": 2363.42, "z": 67.32}, {"x": 5168.92, "y": 2364.4, "z": 67.26}], "right_lane_mark_type": "NONE", "successors": [38120641], "predecessors": [38120362], "right_neighbor_id": null, "left_neighbor_id": null}, "38120064": {"id": 38120064, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5174.94, "y": 2357.99, "z": 67.64}, {"x": 5205.22, "y": 2339.87, "z": 69.63}, {"x": 5209.29, "y": 2337.42, "z": 69.87}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5173.36, "y": 2355.34, "z": 67.67}, {"x": 5207.16, "y": 2335.32, "z": 69.86}, {"x": 5207.69, "y": 2334.98, "z": 69.87}], "right_lane_mark_type": "NONE", "successors": [38114355, 38114407], "predecessors": [38120363, 38120167], "right_neighbor_id": null, "left_neighbor_id": null}, "38120167": {"id": 38120167, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5167.66, "y": 2362.35, "z": 67.19}, {"x": 5174.94, "y": 2357.99, "z": 67.64}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5166.06, "y": 2359.67, "z": 67.17}, {"x": 5173.36, "y": 2355.34, "z": 67.67}], "right_lane_mark_type": "NONE", "successors": [38120064], "predecessors": [38114336], "right_neighbor_id": null, "left_neighbor_id": null}, "38120280": {"id": 38120280, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5170.73, "y": 2355.66, "z": 67.47}, {"x": 5166.47, "y": 2348.63, "z": 67.35}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5167.63, "y": 2357.63, "z": 67.27}, {"x": 5163.46, "y": 2350.39, "z": 67.17}], "right_lane_mark_type": "NONE", "successors": [], "predecessors": [38119984, 38120430], "right_neighbor_id": null, "left_neighbor_id": null}, "38120362": {"id": 38120362, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5163.44, "y": 2350.35, "z": 67.17}, {"x": 5165.31, "y": 2353.6, "z": 67.18}, {"x": 5165.65, "y": 2354.2, "z": 67.19}, {"x": 5167.65, "y": 2357.66, "z": 67.27}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5166.49, "y": 2348.66, "z": 67.35}, {"x": 5169.74, "y": 2354.03, "z": 67.51}, {"x": 5169.89, "y": 2354.46, "z": 67.46}, {"x": 5170.71, "y": 2355.63, "z": 67.47}], "right_lane_mark_type": "NONE", "successors": [38120026, 38120363], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "38120363": {"id": 38120363, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5167.65, "y": 2357.66, "z": 67.27}, {"x": 5168.81, "y": 2358.7, "z": 67.31}, {"x": 5170.09, "y": 2359.26, "z": 67.35}, {"x": 5171.37, "y": 2359.43, "z": 67.4}, {"x": 5172.53, "y": 2359.27, "z": 67.48}, {"x": 5173.47, "y": 2358.87, "z": 67.55}, {"x": 5174.94, "y": 2357.99, "z": 67.64}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5170.71, "y": 2355.63, "z": 67.47}, {"x": 5171.02, "y": 2356.14, "z": 67.47}, {"x": 5171.69, "y": 2356.32, "z": 67.51}, {"x": 5171.88, "y": 2356.21, "z": 67.54}, {"x": 5173.36, "y": 2355.34, "z": 67.67}], "right_lane_mark_type": "NONE", "successors": [38120064], "predecessors": [38120362], "right_neighbor_id": null, "left_neighbor_id": null}, "38120430": {"id": 38120430, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5167.66, "y": 2362.35, "z": 67.19}, {"x": 5169.08, "y": 2361.5, "z": 67.27}, {"x": 5170.07, "y": 2360.68, "z": 67.33}, {"x": 5170.76, "y": 2359.53, "z": 67.37}, {"x": 5171.15, "y": 2356.87, "z": 67.44}, {"x": 5170.73, "y": 2355.66, "z": 67.47}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5166.06, "y": 2359.67, "z": 67.17}, {"x": 5167.72, "y": 2358.69, "z": 67.27}, {"x": 5167.82, "y": 2357.96, "z": 67.29}, {"x": 5167.63, "y": 2357.63, "z": 67.27}], "right_lane_mark_type": "NONE", "successors": [38120280], "predecessors": [38114336], "right_neighbor_id": null, "left_neighbor_id": null}, "38120641": {"id": 38120641, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5166.88, "y": 2361.07, "z": 67.17}, {"x": 5151.93, "y": 2370.0, "z": 66.2}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5168.92, "y": 2364.4, "z": 67.26}, {"x": 5160.02, "y": 2370.0, "z": 66.66}], "right_lane_mark_type": "NONE", "successors": [38119950], "predecessors": [38120769, 38120026], "right_neighbor_id": null, "left_neighbor_id": null}, "38120769": {"id": 38120769, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5174.16, "y": 2356.73, "z": 67.63}, {"x": 5166.88, "y": 2361.07, "z": 67.17}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5175.92, "y": 2359.61, "z": 67.7}, {"x": 5171.86, "y": 2362.15, "z": 67.45}, {"x": 5171.4, "y": 2362.4, "z": 67.42}, {"x": 5170.95, "y": 2362.59, "z": 67.38}, {"x": 5170.61, "y": 2362.78, "z": 67.36}, {"x": 5170.52, "y": 2363.31, "z": 67.35}, {"x": 5168.92, "y": 2364.4, "z": 67.26}], "right_lane_mark_type": "NONE", "successors": [38120641], "predecessors": [38114335], "right_neighbor_id": null, "left_neighbor_id": null}, "38133153": {"id": 38133153, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5178.75, "y": 2414.34, "z": 66.8}, {"x": 5203.94, "y": 2397.25, "z": 67.92}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5175.57, "y": 2409.78, "z": 66.79}, {"x": 5178.53, "y": 2407.76, "z": 66.9}, {"x": 5185.09, "y": 2403.28, "z": 67.18}, {"x": 5200.63, "y": 2392.74, "z": 67.86}], "right_lane_mark_type": "NONE", "successors": [38114433], "predecessors": [38133155], "right_neighbor_id": null, "left_neighbor_id": 38133156}, "38133154": {"id": 38133154, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5164.69, "y": 2425.75, "z": 66.29}, {"x": 5180.46, "y": 2416.73, "z": 66.82}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5160.51, "y": 2419.95, "z": 66.15}, {"x": 5178.75, "y": 2414.34, "z": 66.8}], "right_lane_mark_type": "NONE", "successors": [38133156], "predecessors": [38111243, 38111879], "right_neighbor_id": null, "left_neighbor_id": null}, "38133155": {"id": 38133155, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5164.69, "y": 2425.75, "z": 66.29}, {"x": 5178.75, "y": 2414.34, "z": 66.8}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 5160.51, "y": 2419.95, "z": 66.15}, {"x": 5173.62, "y": 2411.12, "z": 66.71}, {"x": 5175.57, "y": 2409.78, "z": 66.79}], "right_lane_mark_type": "NONE", "successors": [38133153], "predecessors": [38111243, 38111879], "right_neighbor_id": null, "left_neighbor_id": null}, "38133156": {"id": 38133156, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 5180.46, "y": 2416.73, "z": 66.82}, {"x": 5185.87, "y": 2413.3, "z": 67.05}, {"x": 5205.75, "y": 2399.69, "z": 67.93}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 5178.75, "y": 2414.34, "z": 66.8}, {"x": 5203.94, "y": 2397.25, "z": 67.92}], "right_lane_mark_type": "NONE", "successors": [38114426], "predecessors": [38133154], "right_neighbor_id": 38133153, "left_neighbor_id": 38110982}}, "drivable_areas": {"1225617": {"area_boundary": [{"x": 5294.97, "y": 2281.98, "z": 72.82}, {"x": 5261.25, "y": 2304.78, "z": 71.77}, {"x": 5262.87, "y": 2306.91, "z": 71.77}, {"x": 5296.48, "y": 2284.83, "z": 72.77}], "id": 1225617}, "1225544": {"area_boundary": [{"x": 5250.0, "y": 2299.5, "z": 71.78}, {"x": 5254.87, "y": 2307.16, "z": 71.61}, {"x": 5254.89, "y": 2307.87, "z": 71.61}, {"x": 5254.53, "y": 2308.51, "z": 71.63}, {"x": 5253.92, "y": 2309.64, "z": 71.63}, {"x": 5252.61, "y": 2311.09, "z": 71.59}, {"x": 5251.54, "y": 2312.09, "z": 71.57}, {"x": 5240.51, "y": 2319.7, "z": 71.41}, {"x": 5224.76, "y": 2328.95, "z": 70.43}, {"x": 5226.56, "y": 2331.45, "z": 70.42}, {"x": 5256.86, "y": 2312.36, "z": 71.57}, {"x": 5257.62, "y": 2311.97, "z": 71.57}, {"x": 5257.95, "y": 2311.81, "z": 71.56}, {"x": 5258.39, "y": 2311.9, "z": 71.56}, {"x": 5258.6, "y": 2312.24, "z": 71.56}, {"x": 5258.45, "y": 2312.7, "z": 71.56}, {"x": 5264.99, "y": 2324.16, "z": 71.38}, {"x": 5265.5, "y": 2324.2, "z": 71.36}, {"x": 5266.02, "y": 2324.61, "z": 71.35}, {"x": 5266.24, "y": 2325.24, "z": 71.34}, {"x": 5266.13, "y": 2325.83, "z": 71.38}, {"x": 5269.43, "y": 2331.55, "z": 71.14}, {"x": 5274.25, "y": 2328.25, "z": 71.24}, {"x": 5268.13, "y": 2319.68, "z": 71.4}, {"x": 5262.15, "y": 2309.28, "z": 71.6}, {"x": 5261.95, "y": 2308.72, "z": 71.61}, {"x": 5261.92, "y": 2308.24, "z": 71.65}, {"x": 5261.92, "y": 2307.71, "z": 71.66}, {"x": 5262.4, "y": 2307.24, "z": 71.72}, {"x": 5262.87, "y": 2306.91, "z": 71.77}, {"x": 5261.25, "y": 2304.78, "z": 71.77}, {"x": 5260.63, "y": 2305.2, "z": 71.72}, {"x": 5259.98, "y": 2305.58, "z": 71.67}, {"x": 5259.51, "y": 2305.63, "z": 71.65}, {"x": 5259.04, "y": 2305.25, "z": 71.64}, {"x": 5258.3, "y": 2304.54, "z": 71.64}, {"x": 5250.0, "y": 2294.7, "z": 71.75}], "id": 1225544}, "1225511": {"area_boundary": [{"x": 5250.0, "y": 2299.5, "z": 71.78}, {"x": 5250.0, "y": 2294.7, "z": 71.75}, {"x": 5242.61, "y": 2282.67, "z": 71.96}, {"x": 5235.17, "y": 2269.07, "z": 72.11}, {"x": 5227.09, "y": 2250.0, "z": 72.42}, {"x": 5220.31, "y": 2250.0, "z": 72.46}, {"x": 5232.87, "y": 2271.08, "z": 72.19}, {"x": 5243.16, "y": 2288.44, "z": 71.97}, {"x": 5249.42, "y": 2299.27, "z": 71.8}], "id": 1225511}, "1224874": {"area_boundary": [{"x": 5080.18, "y": 2464.32, "z": 62.58}, {"x": 5078.09, "y": 2464.14, "z": 62.46}, {"x": 5075.34, "y": 2463.47, "z": 62.32}, {"x": 5072.91, "y": 2462.71, "z": 62.19}, {"x": 5070.14, "y": 2461.67, "z": 62.04}, {"x": 5066.28, "y": 2466.97, "z": 62.14}, {"x": 5068.46, "y": 2468.29, "z": 62.32}, {"x": 5070.52, "y": 2469.0, "z": 62.42}, {"x": 5072.35, "y": 2469.6, "z": 62.54}, {"x": 5074.12, "y": 2470.05, "z": 62.55}, {"x": 5076.67, "y": 2470.57, "z": 62.68}, {"x": 5078.37, "y": 2470.74, "z": 62.76}, {"x": 5086.92, "y": 2464.32, "z": 62.93}, {"x": 5084.06, "y": 2464.5, "z": 62.76}, {"x": 5082.12, "y": 2464.39, "z": 62.67}], "id": 1224874}, "1224872": {"area_boundary": [{"x": 5032.17, "y": 2470.56, "z": 60.83}, {"x": 5026.74, "y": 2468.65, "z": 60.64}, {"x": 5021.65, "y": 2466.92, "z": 60.43}, {"x": 5017.01, "y": 2465.33, "z": 60.29}, {"x": 5010.0, "y": 2463.0, "z": 60.01}, {"x": 5010.0, "y": 2471.68, "z": 60.33}, {"x": 5012.35, "y": 2472.57, "z": 60.4}, {"x": 5018.51, "y": 2474.9, "z": 60.63}, {"x": 5024.16, "y": 2477.05, "z": 60.81}, {"x": 5029.58, "y": 2479.22, "z": 60.97}, {"x": 5032.94, "y": 2480.43, "z": 61.08}, {"x": 5036.87, "y": 2482.05, "z": 61.22}, {"x": 5039.36, "y": 2482.91, "z": 61.31}, {"x": 5041.9, "y": 2483.91, "z": 61.41}, {"x": 5041.65, "y": 2484.42, "z": 61.4}, {"x": 5038.09, "y": 2483.1, "z": 61.26}, {"x": 5032.1, "y": 2480.81, "z": 61.05}, {"x": 5026.13, "y": 2478.59, "z": 60.88}, {"x": 5010.0, "y": 2472.58, "z": 60.31}, {"x": 5010.0, "y": 2479.16, "z": 60.3}, {"x": 5021.51, "y": 2483.6, "z": 60.75}, {"x": 5022.34, "y": 2484.0, "z": 60.76}, {"x": 5022.49, "y": 2484.35, "z": 60.77}, {"x": 5022.39, "y": 2484.82, "z": 60.81}, {"x": 5020.46, "y": 2490.53, "z": 61.09}, {"x": 5030.18, "y": 2494.2, "z": 61.41}, {"x": 5032.12, "y": 2488.32, "z": 61.08}, {"x": 5032.23, "y": 2487.98, "z": 61.06}, {"x": 5032.47, "y": 2487.82, "z": 61.06}, {"x": 5033.48, "y": 2488.16, "z": 61.1}, {"x": 5036.66, "y": 2489.39, "z": 61.23}, {"x": 5044.46, "y": 2492.36, "z": 61.48}, {"x": 5044.99, "y": 2492.6, "z": 61.5}, {"x": 5045.31, "y": 2492.97, "z": 61.5}, {"x": 5045.34, "y": 2493.39, "z": 61.52}, {"x": 5045.12, "y": 2493.95, "z": 61.55}, {"x": 5043.02, "y": 2498.31, "z": 61.72}, {"x": 5048.12, "y": 2500.2, "z": 61.93}, {"x": 5049.07, "y": 2498.09, "z": 61.82}, {"x": 5049.86, "y": 2496.78, "z": 61.79}, {"x": 5050.34, "y": 2496.29, "z": 61.76}, {"x": 5050.81, "y": 2496.19, "z": 61.75}, {"x": 5051.33, "y": 2496.3, "z": 61.77}, {"x": 5051.68, "y": 2496.6, "z": 61.8}, {"x": 5052.49, "y": 2497.28, "z": 61.86}, {"x": 5053.3, "y": 2498.09, "z": 61.93}, {"x": 5053.9, "y": 2498.77, "z": 61.95}, {"x": 5054.4, "y": 2499.48, "z": 62.0}, {"x": 5055.04, "y": 2500.51, "z": 62.02}, {"x": 5055.56, "y": 2501.66, "z": 62.07}, {"x": 5056.12, "y": 2503.21, "z": 62.14}, {"x": 5057.02, "y": 2505.22, "z": 62.22}, {"x": 5057.57, "y": 2506.99, "z": 62.35}, {"x": 5069.33, "y": 2504.24, "z": 62.53}, {"x": 5069.56, "y": 2501.28, "z": 62.48}, {"x": 5070.11, "y": 2501.02, "z": 62.48}, {"x": 5070.17, "y": 2502.82, "z": 62.52}, {"x": 5069.97, "y": 2505.98, "z": 62.6}, {"x": 5069.18, "y": 2509.56, "z": 62.7}, {"x": 5068.14, "y": 2512.67, "z": 62.78}, {"x": 5066.3, "y": 2516.09, "z": 62.85}, {"x": 5065.6, "y": 2517.23, "z": 62.88}, {"x": 5065.26, "y": 2516.98, "z": 62.87}, {"x": 5066.36, "y": 2514.95, "z": 62.83}, {"x": 5057.89, "y": 2512.36, "z": 62.64}, {"x": 5057.83, "y": 2513.45, "z": 62.69}, {"x": 5057.66, "y": 2514.59, "z": 62.76}, {"x": 5057.34, "y": 2516.12, "z": 62.82}, {"x": 5056.83, "y": 2517.76, "z": 62.85}, {"x": 5056.08, "y": 2519.65, "z": 62.91}, {"x": 5055.18, "y": 2521.41, "z": 62.97}, {"x": 5054.37, "y": 2522.67, "z": 63.0}, {"x": 5053.45, "y": 2523.77, "z": 63.04}, {"x": 5051.85, "y": 2525.53, "z": 63.07}, {"x": 5051.39, "y": 2525.57, "z": 63.08}, {"x": 5050.11, "y": 2525.49, "z": 63.07}, {"x": 5049.61, "y": 2525.39, "z": 63.08}, {"x": 5047.85, "y": 2524.72, "z": 63.17}, {"x": 5042.85, "y": 2522.79, "z": 63.22}, {"x": 5042.19, "y": 2522.69, "z": 63.2}, {"x": 5039.48, "y": 2522.45, "z": 63.14}, {"x": 5038.71, "y": 2522.26, "z": 63.11}, {"x": 5010.0, "y": 2510.75, "z": 62.5}, {"x": 5010.0, "y": 2515.69, "z": 62.6}, {"x": 5013.73, "y": 2517.3, "z": 62.69}, {"x": 5019.91, "y": 2519.88, "z": 62.83}, {"x": 5040.96, "y": 2527.69, "z": 63.31}, {"x": 5042.51, "y": 2528.46, "z": 63.34}, {"x": 5043.75, "y": 2529.25, "z": 63.34}, {"x": 5044.57, "y": 2529.95, "z": 63.32}, {"x": 5044.95, "y": 2530.37, "z": 63.31}, {"x": 5045.1, "y": 2530.88, "z": 63.31}, {"x": 5045.23, "y": 2531.45, "z": 63.33}, {"x": 5045.09, "y": 2531.94, "z": 63.35}, {"x": 5044.55, "y": 2532.63, "z": 63.39}, {"x": 5039.8, "y": 2537.2, "z": 63.57}, {"x": 5036.86, "y": 2539.98, "z": 63.66}, {"x": 5030.58, "y": 2545.93, "z": 63.93}, {"x": 5026.3, "y": 2550.0, "z": 64.1}, {"x": 5040.06, "y": 2550.0, "z": 63.92}, {"x": 5049.46, "y": 2540.93, "z": 63.59}, {"x": 5055.0, "y": 2535.78, "z": 63.43}, {"x": 5058.85, "y": 2532.21, "z": 63.25}, {"x": 5059.94, "y": 2531.29, "z": 63.16}, {"x": 5060.53, "y": 2531.14, "z": 63.15}, {"x": 5061.19, "y": 2530.96, "z": 63.12}, {"x": 5062.06, "y": 2530.98, "z": 63.03}, {"x": 5062.61, "y": 2531.04, "z": 62.96}, {"x": 5063.13, "y": 2531.39, "z": 62.95}, {"x": 5064.46, "y": 2532.57, "z": 62.99}, {"x": 5068.87, "y": 2537.16, "z": 63.18}, {"x": 5069.88, "y": 2538.22, "z": 63.24}, {"x": 5071.15, "y": 2539.45, "z": 63.34}, {"x": 5072.36, "y": 2540.65, "z": 63.41}, {"x": 5074.35, "y": 2542.69, "z": 63.52}, {"x": 5076.31, "y": 2544.76, "z": 63.57}, {"x": 5076.37, "y": 2545.29, "z": 63.59}, {"x": 5075.81, "y": 2545.84, "z": 63.61}, {"x": 5074.04, "y": 2547.26, "z": 63.66}, {"x": 5070.49, "y": 2550.15, "z": 63.79}, {"x": 5070.5, "y": 2560.51, "z": 64.08}, {"x": 5079.14, "y": 2553.89, "z": 63.95}, {"x": 5082.58, "y": 2551.11, "z": 63.82}, {"x": 5083.24, "y": 2551.01, "z": 63.84}, {"x": 5083.64, "y": 2551.15, "z": 63.85}, {"x": 5086.13, "y": 2553.56, "z": 63.95}, {"x": 5086.13, "y": 2553.79, "z": 63.96}, {"x": 5085.96, "y": 2554.08, "z": 63.96}, {"x": 5083.39, "y": 2556.21, "z": 64.19}, {"x": 5088.44, "y": 2560.34, "z": 64.31}, {"x": 5089.85, "y": 2558.34, "z": 64.13}, {"x": 5090.17, "y": 2558.01, "z": 64.1}, {"x": 5090.38, "y": 2557.99, "z": 64.1}, {"x": 5090.6, "y": 2558.04, "z": 64.11}, {"x": 5091.01, "y": 2558.36, "z": 64.12}, {"x": 5100.0, "y": 2566.86, "z": 64.47}, {"x": 5100.0, "y": 2552.91, "z": 64.19}, {"x": 5095.83, "y": 2548.85, "z": 64.03}, {"x": 5091.83, "y": 2544.81, "z": 63.76}, {"x": 5091.55, "y": 2544.01, "z": 63.73}, {"x": 5091.62, "y": 2543.16, "z": 63.71}, {"x": 5094.28, "y": 2541.03, "z": 63.93}, {"x": 5085.8, "y": 2532.0, "z": 63.76}, {"x": 5082.86, "y": 2535.04, "z": 63.58}, {"x": 5082.26, "y": 2535.19, "z": 63.51}, {"x": 5081.6, "y": 2535.01, "z": 63.47}, {"x": 5080.9, "y": 2534.43, "z": 63.45}, {"x": 5080.0, "y": 2533.48, "z": 63.41}, {"x": 5078.98, "y": 2532.26, "z": 63.37}, {"x": 5078.14, "y": 2530.91, "z": 63.23}, {"x": 5077.24, "y": 2529.31, "z": 63.21}, {"x": 5076.66, "y": 2527.93, "z": 63.19}, {"x": 5076.34, "y": 2526.95, "z": 63.06}, {"x": 5076.14, "y": 2525.81, "z": 62.99}, {"x": 5075.89, "y": 2523.71, "z": 63.03}, {"x": 5075.78, "y": 2522.41, "z": 63.03}, {"x": 5075.78, "y": 2521.66, "z": 63.02}, {"x": 5075.79, "y": 2520.75, "z": 62.98}, {"x": 5075.92, "y": 2519.93, "z": 62.93}, {"x": 5076.18, "y": 2518.58, "z": 62.8}, {"x": 5076.65, "y": 2517.01, "z": 62.77}, {"x": 5077.06, "y": 2515.53, "z": 62.75}, {"x": 5077.4, "y": 2514.64, "z": 62.77}, {"x": 5077.96, "y": 2513.58, "z": 62.78}, {"x": 5078.69, "y": 2512.76, "z": 62.75}, {"x": 5080.14, "y": 2511.48, "z": 62.75}, {"x": 5083.35, "y": 2508.34, "z": 62.81}, {"x": 5093.71, "y": 2498.4, "z": 63.2}, {"x": 5090.57, "y": 2495.35, "z": 63.27}, {"x": 5081.65, "y": 2503.9, "z": 62.81}, {"x": 5080.92, "y": 2504.57, "z": 62.79}, {"x": 5080.52, "y": 2504.59, "z": 62.78}, {"x": 5080.15, "y": 2504.44, "z": 62.78}, {"x": 5079.95, "y": 2503.97, "z": 62.79}, {"x": 5079.99, "y": 2502.45, "z": 62.84}, {"x": 5079.62, "y": 2494.63, "z": 62.8}, {"x": 5079.39, "y": 2493.62, "z": 62.82}, {"x": 5079.29, "y": 2492.68, "z": 62.82}, {"x": 5079.32, "y": 2492.08, "z": 62.8}, {"x": 5079.69, "y": 2491.61, "z": 62.86}, {"x": 5080.79, "y": 2491.17, "z": 62.87}, {"x": 5083.13, "y": 2490.32, "z": 62.98}, {"x": 5090.31, "y": 2487.14, "z": 63.29}, {"x": 5095.81, "y": 2484.07, "z": 63.53}, {"x": 5098.57, "y": 2482.18, "z": 63.67}, {"x": 5100.95, "y": 2480.67, "z": 63.77}, {"x": 5104.65, "y": 2478.19, "z": 63.96}, {"x": 5107.23, "y": 2476.31, "z": 63.98}, {"x": 5112.11, "y": 2473.06, "z": 64.17}, {"x": 5113.22, "y": 2472.37, "z": 64.16}, {"x": 5115.44, "y": 2470.8, "z": 64.19}, {"x": 5116.4, "y": 2470.14, "z": 64.22}, {"x": 5116.58, "y": 2470.14, "z": 64.23}, {"x": 5116.68, "y": 2470.26, "z": 64.23}, {"x": 5116.64, "y": 2470.42, "z": 64.22}, {"x": 5116.53, "y": 2470.59, "z": 64.22}, {"x": 5116.2, "y": 2470.87, "z": 64.2}, {"x": 5113.51, "y": 2473.63, "z": 64.13}, {"x": 5116.98, "y": 2476.3, "z": 64.13}, {"x": 5130.0, "y": 2463.82, "z": 64.67}, {"x": 5130.0, "y": 2440.97, "z": 64.85}, {"x": 5129.86, "y": 2440.85, "z": 64.85}, {"x": 5129.72, "y": 2440.47, "z": 64.89}, {"x": 5129.55, "y": 2439.77, "z": 64.95}, {"x": 5125.08, "y": 2431.97, "z": 64.94}, {"x": 5119.65, "y": 2434.78, "z": 64.85}, {"x": 5122.66, "y": 2439.29, "z": 64.97}, {"x": 5122.9, "y": 2439.92, "z": 64.94}, {"x": 5122.86, "y": 2440.19, "z": 64.93}, {"x": 5122.61, "y": 2440.07, "z": 64.96}, {"x": 5122.21, "y": 2439.68, "z": 64.98}, {"x": 5119.0, "y": 2435.2, "z": 64.85}, {"x": 5115.25, "y": 2438.57, "z": 64.89}, {"x": 5117.72, "y": 2442.16, "z": 65.03}, {"x": 5120.91, "y": 2446.15, "z": 64.54}, {"x": 5121.28, "y": 2446.66, "z": 64.5}, {"x": 5121.3, "y": 2446.87, "z": 64.48}, {"x": 5121.09, "y": 2447.05, "z": 64.47}, {"x": 5120.85, "y": 2447.19, "z": 64.47}, {"x": 5114.69, "y": 2451.39, "z": 64.23}, {"x": 5105.83, "y": 2457.55, "z": 63.87}, {"x": 5101.82, "y": 2460.18, "z": 63.64}, {"x": 5098.84, "y": 2462.02, "z": 63.53}, {"x": 5095.63, "y": 2463.45, "z": 63.37}, {"x": 5093.17, "y": 2463.73, "z": 63.24}, {"x": 5090.35, "y": 2464.03, "z": 63.12}, {"x": 5086.92, "y": 2464.32, "z": 62.93}, {"x": 5078.37, "y": 2470.74, "z": 62.76}, {"x": 5082.09, "y": 2471.08, "z": 62.82}, {"x": 5082.52, "y": 2471.28, "z": 62.84}, {"x": 5082.64, "y": 2471.58, "z": 62.85}, {"x": 5082.68, "y": 2472.05, "z": 62.87}, {"x": 5082.46, "y": 2472.39, "z": 62.87}, {"x": 5077.98, "y": 2474.35, "z": 62.75}, {"x": 5072.97, "y": 2476.07, "z": 62.54}, {"x": 5071.95, "y": 2476.34, "z": 62.52}, {"x": 5071.32, "y": 2476.55, "z": 62.48}, {"x": 5070.79, "y": 2476.57, "z": 62.46}, {"x": 5070.31, "y": 2476.49, "z": 62.42}, {"x": 5069.76, "y": 2476.1, "z": 62.41}, {"x": 5065.87, "y": 2469.36, "z": 62.27}, {"x": 5065.24, "y": 2468.45, "z": 62.16}, {"x": 5065.26, "y": 2467.58, "z": 62.13}, {"x": 5066.28, "y": 2466.97, "z": 62.14}, {"x": 5070.14, "y": 2461.67, "z": 62.04}, {"x": 5068.37, "y": 2460.79, "z": 61.96}, {"x": 5065.93, "y": 2459.47, "z": 61.86}, {"x": 5063.28, "y": 2457.77, "z": 61.72}, {"x": 5061.62, "y": 2456.47, "z": 61.62}, {"x": 5060.36, "y": 2455.18, "z": 61.55}, {"x": 5059.46, "y": 2454.16, "z": 61.54}, {"x": 5049.97, "y": 2460.56, "z": 61.83}, {"x": 5051.95, "y": 2463.97, "z": 61.85}, {"x": 5054.18, "y": 2467.71, "z": 61.87}, {"x": 5054.77, "y": 2468.92, "z": 61.91}, {"x": 5054.53, "y": 2469.42, "z": 61.91}, {"x": 5054.02, "y": 2469.45, "z": 61.91}, {"x": 5053.65, "y": 2469.16, "z": 61.9}, {"x": 5052.29, "y": 2467.46, "z": 61.86}, {"x": 5045.62, "y": 2457.65, "z": 61.88}, {"x": 5040.42, "y": 2460.7, "z": 61.69}, {"x": 5045.85, "y": 2471.74, "z": 61.49}, {"x": 5046.59, "y": 2474.34, "z": 61.43}, {"x": 5046.69, "y": 2475.27, "z": 61.42}, {"x": 5046.67, "y": 2475.47, "z": 61.42}, {"x": 5046.49, "y": 2475.48, "z": 61.42}, {"x": 5045.75, "y": 2475.22, "z": 61.39}, {"x": 5044.45, "y": 2474.77, "z": 61.36}, {"x": 5038.78, "y": 2472.89, "z": 61.13}], "id": 1224872}, "1224856": {"area_boundary": [{"x": 5199.57, "y": 2529.87, "z": 67.0}, {"x": 5203.86, "y": 2525.95, "z": 67.11}, {"x": 5208.38, "y": 2521.71, "z": 67.2}, {"x": 5209.63, "y": 2520.48, "z": 67.22}, {"x": 5210.84, "y": 2519.43, "z": 67.24}, {"x": 5211.62, "y": 2518.98, "z": 67.26}, {"x": 5212.21, "y": 2518.97, "z": 67.26}, {"x": 5212.64, "y": 2519.22, "z": 67.28}, {"x": 5213.29, "y": 2519.94, "z": 67.29}, {"x": 5214.58, "y": 2521.78, "z": 67.35}, {"x": 5222.69, "y": 2533.31, "z": 67.64}, {"x": 5236.46, "y": 2552.62, "z": 68.05}, {"x": 5236.81, "y": 2553.16, "z": 68.05}, {"x": 5236.91, "y": 2553.66, "z": 68.06}, {"x": 5236.76, "y": 2554.07, "z": 68.07}, {"x": 5225.57, "y": 2564.27, "z": 68.08}, {"x": 5230.46, "y": 2569.79, "z": 68.17}, {"x": 5240.38, "y": 2560.52, "z": 68.24}, {"x": 5240.88, "y": 2560.09, "z": 68.22}, {"x": 5241.24, "y": 2559.96, "z": 68.22}, {"x": 5241.66, "y": 2560.02, "z": 68.22}, {"x": 5242.08, "y": 2560.25, "z": 68.22}, {"x": 5242.53, "y": 2560.78, "z": 68.24}, {"x": 5244.14, "y": 2563.17, "z": 68.27}, {"x": 5255.86, "y": 2579.29, "z": 68.63}, {"x": 5262.65, "y": 2574.86, "z": 68.76}, {"x": 5254.79, "y": 2563.93, "z": 68.51}, {"x": 5249.46, "y": 2556.5, "z": 68.36}, {"x": 5249.16, "y": 2556.08, "z": 68.31}, {"x": 5249.0, "y": 2555.67, "z": 68.31}, {"x": 5249.01, "y": 2555.19, "z": 68.32}, {"x": 5249.16, "y": 2554.67, "z": 68.32}, {"x": 5249.56, "y": 2553.98, "z": 68.35}, {"x": 5280.0, "y": 2525.9, "z": 69.68}, {"x": 5280.0, "y": 2518.26, "z": 69.7}, {"x": 5278.86, "y": 2519.33, "z": 69.67}, {"x": 5278.33, "y": 2519.39, "z": 69.66}, {"x": 5277.86, "y": 2519.27, "z": 69.65}, {"x": 5277.46, "y": 2518.96, "z": 69.63}, {"x": 5272.33, "y": 2512.39, "z": 69.46}, {"x": 5261.53, "y": 2498.96, "z": 69.21}, {"x": 5258.93, "y": 2501.21, "z": 69.24}, {"x": 5263.27, "y": 2507.46, "z": 69.34}, {"x": 5266.98, "y": 2513.23, "z": 69.42}, {"x": 5269.41, "y": 2516.8, "z": 69.5}, {"x": 5271.34, "y": 2519.12, "z": 69.55}, {"x": 5274.04, "y": 2522.49, "z": 69.54}, {"x": 5274.19, "y": 2522.95, "z": 69.54}, {"x": 5274.12, "y": 2523.49, "z": 69.51}, {"x": 5273.94, "y": 2523.89, "z": 69.48}, {"x": 5249.59, "y": 2546.91, "z": 68.47}, {"x": 5248.48, "y": 2547.77, "z": 68.43}, {"x": 5246.6, "y": 2548.97, "z": 68.27}, {"x": 5246.04, "y": 2549.2, "z": 68.23}, {"x": 5245.45, "y": 2549.32, "z": 68.19}, {"x": 5244.88, "y": 2549.26, "z": 68.17}, {"x": 5244.21, "y": 2548.96, "z": 68.15}, {"x": 5243.74, "y": 2548.49, "z": 68.16}, {"x": 5235.02, "y": 2536.19, "z": 67.92}, {"x": 5218.82, "y": 2513.84, "z": 67.41}, {"x": 5210.69, "y": 2502.5, "z": 67.13}, {"x": 5201.69, "y": 2490.0, "z": 66.87}, {"x": 5191.82, "y": 2490.0, "z": 66.62}, {"x": 5206.63, "y": 2510.74, "z": 67.09}, {"x": 5207.16, "y": 2511.57, "z": 67.1}, {"x": 5207.12, "y": 2512.27, "z": 67.11}, {"x": 5206.86, "y": 2513.0, "z": 67.11}, {"x": 5206.27, "y": 2513.59, "z": 67.12}, {"x": 5205.37, "y": 2514.31, "z": 67.14}, {"x": 5194.22, "y": 2524.62, "z": 66.88}], "id": 1224856}, "1224536": {"area_boundary": [{"x": 5030.01, "y": 2220.0, "z": 60.98}, {"x": 5020.71, "y": 2220.0, "z": 60.75}, {"x": 5022.66, "y": 2223.31, "z": 60.91}, {"x": 5047.06, "y": 2263.43, "z": 62.11}, {"x": 5047.38, "y": 2263.95, "z": 62.12}, {"x": 5047.53, "y": 2264.48, "z": 62.14}, {"x": 5047.44, "y": 2264.97, "z": 62.15}, {"x": 5047.15, "y": 2265.39, "z": 62.17}, {"x": 5046.74, "y": 2265.71, "z": 62.19}, {"x": 5044.02, "y": 2267.26, "z": 62.21}, {"x": 5043.77, "y": 2267.15, "z": 62.2}, {"x": 5043.38, "y": 2266.74, "z": 62.16}, {"x": 5042.92, "y": 2266.77, "z": 62.13}, {"x": 5023.67, "y": 2278.5, "z": 60.41}, {"x": 5021.7, "y": 2279.55, "z": 60.13}, {"x": 5020.64, "y": 2279.93, "z": 59.99}, {"x": 5019.7, "y": 2280.18, "z": 59.88}, {"x": 5018.72, "y": 2280.15, "z": 59.78}, {"x": 5018.03, "y": 2279.84, "z": 59.71}, {"x": 5017.43, "y": 2279.44, "z": 59.68}, {"x": 5016.86, "y": 2278.84, "z": 59.64}, {"x": 5016.19, "y": 2278.01, "z": 59.55}, {"x": 4999.75, "y": 2250.0, "z": 58.84}, {"x": 4992.92, "y": 2250.0, "z": 58.86}, {"x": 5011.6, "y": 2280.68, "z": 59.52}, {"x": 5011.63, "y": 2281.15, "z": 59.53}, {"x": 5011.48, "y": 2281.49, "z": 59.52}, {"x": 5010.79, "y": 2282.0, "z": 59.5}, {"x": 5009.73, "y": 2282.67, "z": 59.49}, {"x": 5004.83, "y": 2285.64, "z": 59.37}, {"x": 4988.93, "y": 2295.12, "z": 58.8}, {"x": 4990.2, "y": 2298.45, "z": 58.58}, {"x": 4990.2, "y": 2298.78, "z": 58.55}, {"x": 4990.07, "y": 2299.04, "z": 58.51}, {"x": 4989.24, "y": 2299.57, "z": 58.43}, {"x": 4988.73, "y": 2299.76, "z": 58.39}, {"x": 4988.35, "y": 2299.8, "z": 58.37}, {"x": 4987.95, "y": 2299.71, "z": 58.35}, {"x": 4987.6, "y": 2299.53, "z": 58.32}, {"x": 4987.28, "y": 2299.24, "z": 58.29}, {"x": 4969.42, "y": 2270.1, "z": 58.11}, {"x": 4957.26, "y": 2250.0, "z": 58.07}, {"x": 4949.58, "y": 2250.0, "z": 58.06}, {"x": 4964.17, "y": 2273.74, "z": 58.16}, {"x": 4970.14, "y": 2283.76, "z": 58.19}, {"x": 4981.92, "y": 2303.09, "z": 58.28}, {"x": 4983.28, "y": 2305.36, "z": 58.32}, {"x": 4983.69, "y": 2305.81, "z": 58.33}, {"x": 4984.23, "y": 2306.09, "z": 58.34}, {"x": 4984.91, "y": 2306.16, "z": 58.36}, {"x": 4985.5, "y": 2305.89, "z": 58.37}, {"x": 4987.7, "y": 2304.61, "z": 58.49}, {"x": 5046.16, "y": 2269.51, "z": 62.25}, {"x": 5047.08, "y": 2268.83, "z": 62.27}, {"x": 5048.45, "y": 2268.01, "z": 62.23}, {"x": 5048.87, "y": 2267.88, "z": 62.23}, {"x": 5049.33, "y": 2267.85, "z": 62.22}, {"x": 5049.68, "y": 2267.94, "z": 62.22}, {"x": 5049.98, "y": 2268.18, "z": 62.22}, {"x": 5055.99, "y": 2278.14, "z": 62.39}, {"x": 5066.04, "y": 2294.68, "z": 62.7}, {"x": 5069.43, "y": 2300.17, "z": 62.8}, {"x": 5079.4, "y": 2316.56, "z": 63.2}, {"x": 5092.18, "y": 2337.58, "z": 63.53}, {"x": 5103.73, "y": 2356.66, "z": 63.84}, {"x": 5111.89, "y": 2370.0, "z": 64.14}, {"x": 5121.31, "y": 2370.0, "z": 64.41}, {"x": 5111.53, "y": 2354.08, "z": 64.06}, {"x": 5094.14, "y": 2325.55, "z": 63.5}, {"x": 5086.44, "y": 2312.85, "z": 63.25}, {"x": 5076.96, "y": 2297.12, "z": 62.91}, {"x": 5076.69, "y": 2296.53, "z": 62.91}, {"x": 5076.74, "y": 2295.85, "z": 62.92}, {"x": 5076.87, "y": 2295.19, "z": 62.91}, {"x": 5077.09, "y": 2294.56, "z": 62.88}, {"x": 5077.53, "y": 2294.12, "z": 62.87}, {"x": 5100.0, "y": 2280.38, "z": 65.22}, {"x": 5100.0, "y": 2271.98, "z": 65.56}, {"x": 5073.16, "y": 2288.44, "z": 62.79}, {"x": 5072.7, "y": 2288.64, "z": 62.78}, {"x": 5072.2, "y": 2288.63, "z": 62.77}, {"x": 5071.82, "y": 2288.47, "z": 62.77}, {"x": 5071.57, "y": 2288.16, "z": 62.77}], "id": 1224536}, "1224530": {"area_boundary": [{"x": 5159.02, "y": 2247.95, "z": 71.45}, {"x": 5169.18, "y": 2264.82, "z": 71.1}, {"x": 5177.91, "y": 2279.1, "z": 70.76}, {"x": 5190.0, "y": 2299.06, "z": 70.41}, {"x": 5190.0, "y": 2287.64, "z": 70.64}, {"x": 5188.46, "y": 2285.2, "z": 70.69}, {"x": 5180.63, "y": 2272.35, "z": 70.95}, {"x": 5171.26, "y": 2256.75, "z": 71.26}, {"x": 5164.1, "y": 2244.91, "z": 71.5}, {"x": 5163.29, "y": 2243.55, "z": 71.54}, {"x": 5163.14, "y": 2243.14, "z": 71.55}, {"x": 5163.13, "y": 2242.81, "z": 71.56}, {"x": 5163.2, "y": 2242.41, "z": 71.57}, {"x": 5163.37, "y": 2242.07, "z": 71.58}, {"x": 5163.69, "y": 2241.78, "z": 71.59}, {"x": 5166.98, "y": 2239.71, "z": 71.7}, {"x": 5179.72, "y": 2231.94, "z": 72.09}, {"x": 5200.05, "y": 2219.53, "z": 72.67}, {"x": 5200.47, "y": 2219.39, "z": 72.68}, {"x": 5201.01, "y": 2219.41, "z": 72.68}, {"x": 5201.43, "y": 2219.67, "z": 72.71}, {"x": 5201.72, "y": 2220.04, "z": 72.73}, {"x": 5220.31, "y": 2250.0, "z": 72.46}, {"x": 5227.09, "y": 2250.0, "z": 72.42}, {"x": 5220.69, "y": 2240.03, "z": 72.56}, {"x": 5206.93, "y": 2217.38, "z": 72.86}, {"x": 5206.72, "y": 2217.01, "z": 72.86}, {"x": 5206.56, "y": 2216.67, "z": 72.85}, {"x": 5206.52, "y": 2216.26, "z": 72.85}, {"x": 5206.66, "y": 2215.86, "z": 72.86}, {"x": 5206.85, "y": 2215.53, "z": 72.87}, {"x": 5207.19, "y": 2215.27, "z": 72.89}, {"x": 5220.0, "y": 2207.43, "z": 73.28}, {"x": 5220.0, "y": 2199.21, "z": 73.5}, {"x": 5203.77, "y": 2209.03, "z": 72.92}, {"x": 5203.21, "y": 2209.34, "z": 72.91}, {"x": 5202.75, "y": 2209.46, "z": 72.89}, {"x": 5202.35, "y": 2209.37, "z": 72.88}, {"x": 5202.02, "y": 2209.09, "z": 72.88}, {"x": 5201.7, "y": 2208.61, "z": 72.89}, {"x": 5201.34, "y": 2207.92, "z": 72.88}, {"x": 5190.16, "y": 2190.0, "z": 72.36}, {"x": 5183.74, "y": 2190.0, "z": 72.26}, {"x": 5188.0, "y": 2197.2, "z": 72.51}, {"x": 5188.43, "y": 2197.06, "z": 72.5}, {"x": 5188.72, "y": 2197.19, "z": 72.49}, {"x": 5188.95, "y": 2197.49, "z": 72.5}, {"x": 5190.51, "y": 2200.13, "z": 72.58}, {"x": 5190.61, "y": 2200.42, "z": 72.59}, {"x": 5190.55, "y": 2200.68, "z": 72.6}, {"x": 5190.32, "y": 2201.04, "z": 72.61}, {"x": 5196.85, "y": 2211.92, "z": 72.75}, {"x": 5196.99, "y": 2212.33, "z": 72.74}, {"x": 5197.02, "y": 2212.79, "z": 72.71}, {"x": 5196.78, "y": 2213.22, "z": 72.7}, {"x": 5196.27, "y": 2213.57, "z": 72.69}, {"x": 5195.78, "y": 2213.85, "z": 72.67}, {"x": 5194.9, "y": 2214.34, "z": 72.66}, {"x": 5178.8, "y": 2224.17, "z": 72.18}, {"x": 5161.88, "y": 2234.51, "z": 71.68}, {"x": 5160.81, "y": 2235.1, "z": 71.65}, {"x": 5159.97, "y": 2235.55, "z": 71.62}, {"x": 5159.47, "y": 2235.61, "z": 71.6}, {"x": 5159.1, "y": 2235.63, "z": 71.58}, {"x": 5158.76, "y": 2235.55, "z": 71.57}, {"x": 5158.46, "y": 2235.38, "z": 71.56}, {"x": 5158.13, "y": 2235.07, "z": 71.53}, {"x": 5157.57, "y": 2234.27, "z": 71.53}, {"x": 5156.81, "y": 2233.0, "z": 71.47}, {"x": 5153.41, "y": 2227.49, "z": 71.12}, {"x": 5150.24, "y": 2222.29, "z": 70.77}, {"x": 5148.91, "y": 2220.0, "z": 70.65}, {"x": 5141.94, "y": 2220.0, "z": 70.48}, {"x": 5144.42, "y": 2224.07, "z": 70.75}, {"x": 5149.97, "y": 2233.1, "z": 71.28}, {"x": 5152.01, "y": 2236.54, "z": 71.47}, {"x": 5152.74, "y": 2237.7, "z": 71.5}, {"x": 5153.16, "y": 2238.48, "z": 71.51}, {"x": 5153.27, "y": 2238.91, "z": 71.51}, {"x": 5153.3, "y": 2239.25, "z": 71.51}, {"x": 5153.19, "y": 2239.53, "z": 71.49}, {"x": 5153.06, "y": 2239.69, "z": 71.47}, {"x": 5152.81, "y": 2239.91, "z": 71.46}, {"x": 5152.52, "y": 2240.09, "z": 71.44}, {"x": 5151.87, "y": 2240.47, "z": 71.39}, {"x": 5130.0, "y": 2253.72, "z": 68.74}, {"x": 5130.0, "y": 2262.26, "z": 68.28}, {"x": 5154.35, "y": 2247.33, "z": 71.28}, {"x": 5155.95, "y": 2246.42, "z": 71.41}, {"x": 5156.37, "y": 2246.18, "z": 71.43}, {"x": 5156.76, "y": 2246.03, "z": 71.45}, {"x": 5157.19, "y": 2245.97, "z": 71.46}, {"x": 5157.62, "y": 2246.07, "z": 71.47}, {"x": 5157.98, "y": 2246.34, "z": 71.48}, {"x": 5158.25, "y": 2246.66, "z": 71.48}, {"x": 5158.55, "y": 2247.12, "z": 71.47}], "id": 1224530}, "1224529": {"area_boundary": [{"x": 5364.74, "y": 2400.0, "z": 71.28}, {"x": 5348.92, "y": 2369.84, "z": 71.51}, {"x": 5347.72, "y": 2367.22, "z": 71.57}, {"x": 5347.81, "y": 2366.56, "z": 71.55}, {"x": 5348.45, "y": 2365.74, "z": 71.53}, {"x": 5349.76, "y": 2364.93, "z": 71.6}, {"x": 5370.0, "y": 2350.99, "z": 71.64}, {"x": 5370.0, "y": 2346.04, "z": 71.66}, {"x": 5346.12, "y": 2362.43, "z": 71.58}, {"x": 5345.43, "y": 2362.28, "z": 71.6}, {"x": 5344.82, "y": 2361.65, "z": 71.64}, {"x": 5326.71, "y": 2327.63, "z": 72.01}, {"x": 5326.18, "y": 2326.73, "z": 71.98}, {"x": 5325.92, "y": 2325.95, "z": 71.95}, {"x": 5325.89, "y": 2325.05, "z": 71.94}, {"x": 5326.18, "y": 2324.44, "z": 71.96}, {"x": 5326.92, "y": 2323.73, "z": 72.01}, {"x": 5340.0, "y": 2314.77, "z": 71.9}, {"x": 5340.0, "y": 2297.44, "z": 72.1}, {"x": 5325.64, "y": 2307.09, "z": 72.15}, {"x": 5325.08, "y": 2307.48, "z": 72.17}, {"x": 5324.63, "y": 2307.63, "z": 72.16}, {"x": 5324.09, "y": 2307.71, "z": 72.14}, {"x": 5323.46, "y": 2307.73, "z": 72.16}, {"x": 5322.85, "y": 2307.67, "z": 72.17}, {"x": 5322.41, "y": 2307.47, "z": 72.19}, {"x": 5321.7, "y": 2306.78, "z": 72.26}, {"x": 5319.92, "y": 2304.15, "z": 72.46}, {"x": 5309.54, "y": 2286.99, "z": 72.78}, {"x": 5302.77, "y": 2275.8, "z": 72.93}, {"x": 5291.83, "y": 2257.7, "z": 73.17}, {"x": 5287.46, "y": 2250.5, "z": 73.29}, {"x": 5287.41, "y": 2250.24, "z": 73.29}, {"x": 5287.43, "y": 2249.99, "z": 73.29}, {"x": 5287.51, "y": 2249.79, "z": 73.31}, {"x": 5287.77, "y": 2249.6, "z": 73.33}, {"x": 5292.98, "y": 2247.14, "z": 73.38}, {"x": 5288.96, "y": 2237.76, "z": 73.44}, {"x": 5283.04, "y": 2240.95, "z": 73.43}, {"x": 5282.55, "y": 2241.14, "z": 73.42}, {"x": 5282.21, "y": 2241.17, "z": 73.41}, {"x": 5281.97, "y": 2241.12, "z": 73.41}, {"x": 5281.79, "y": 2241.02, "z": 73.41}, {"x": 5281.61, "y": 2240.84, "z": 73.4}, {"x": 5279.01, "y": 2236.55, "z": 73.49}, {"x": 5273.12, "y": 2226.85, "z": 73.61}, {"x": 5272.68, "y": 2226.13, "z": 73.66}, {"x": 5272.51, "y": 2225.7, "z": 73.66}, {"x": 5272.5, "y": 2225.36, "z": 73.67}, {"x": 5272.62, "y": 2225.02, "z": 73.69}, {"x": 5272.92, "y": 2224.77, "z": 73.71}, {"x": 5276.72, "y": 2222.89, "z": 73.71}, {"x": 5275.57, "y": 2220.13, "z": 73.71}, {"x": 5270.46, "y": 2220.8, "z": 73.77}, {"x": 5269.87, "y": 2220.81, "z": 73.75}, {"x": 5269.64, "y": 2220.72, "z": 73.75}, {"x": 5269.44, "y": 2220.57, "z": 73.76}, {"x": 5269.2, "y": 2220.32, "z": 73.76}, {"x": 5268.97, "y": 2220.0, "z": 73.76}, {"x": 5260.63, "y": 2220.0, "z": 73.76}, {"x": 5260.68, "y": 2220.35, "z": 73.76}, {"x": 5260.74, "y": 2220.69, "z": 73.75}, {"x": 5260.74, "y": 2220.99, "z": 73.73}, {"x": 5260.62, "y": 2221.41, "z": 73.74}, {"x": 5260.32, "y": 2221.86, "z": 73.75}, {"x": 5259.27, "y": 2222.8, "z": 73.78}, {"x": 5256.07, "y": 2225.4, "z": 73.82}, {"x": 5262.44, "y": 2234.05, "z": 73.77}, {"x": 5266.19, "y": 2231.2, "z": 73.59}, {"x": 5266.65, "y": 2230.91, "z": 73.56}, {"x": 5267.0, "y": 2230.88, "z": 73.54}, {"x": 5267.25, "y": 2230.98, "z": 73.54}, {"x": 5267.45, "y": 2231.22, "z": 73.53}, {"x": 5270.88, "y": 2237.03, "z": 73.43}, {"x": 5278.1, "y": 2248.85, "z": 73.27}, {"x": 5284.55, "y": 2259.36, "z": 73.16}, {"x": 5293.45, "y": 2274.0, "z": 72.93}, {"x": 5296.37, "y": 2278.93, "z": 72.86}, {"x": 5296.66, "y": 2279.45, "z": 72.83}, {"x": 5296.85, "y": 2279.88, "z": 72.81}, {"x": 5296.88, "y": 2280.24, "z": 72.81}, {"x": 5296.8, "y": 2280.57, "z": 72.8}, {"x": 5296.61, "y": 2280.85, "z": 72.8}, {"x": 5296.14, "y": 2281.23, "z": 72.8}, {"x": 5294.97, "y": 2281.98, "z": 72.82}, {"x": 5296.48, "y": 2284.83, "z": 72.77}, {"x": 5296.97, "y": 2284.57, "z": 72.76}, {"x": 5297.47, "y": 2284.34, "z": 72.73}, {"x": 5297.85, "y": 2284.3, "z": 72.73}, {"x": 5298.26, "y": 2284.28, "z": 72.72}, {"x": 5298.69, "y": 2284.3, "z": 72.72}, {"x": 5299.17, "y": 2284.43, "z": 72.72}, {"x": 5299.65, "y": 2284.72, "z": 72.73}, {"x": 5300.1, "y": 2285.17, "z": 72.72}, {"x": 5300.42, "y": 2285.62, "z": 72.73}, {"x": 5300.84, "y": 2286.37, "z": 72.72}, {"x": 5313.12, "y": 2306.56, "z": 72.4}, {"x": 5315.43, "y": 2310.41, "z": 72.23}, {"x": 5316.06, "y": 2311.32, "z": 72.17}, {"x": 5316.31, "y": 2311.84, "z": 72.13}, {"x": 5316.44, "y": 2312.35, "z": 72.11}, {"x": 5316.43, "y": 2312.89, "z": 72.08}, {"x": 5316.19, "y": 2313.54, "z": 72.06}, {"x": 5315.74, "y": 2314.1, "z": 72.04}, {"x": 5314.22, "y": 2315.02, "z": 72.0}, {"x": 5281.75, "y": 2337.19, "z": 70.96}, {"x": 5280.92, "y": 2337.55, "z": 70.94}, {"x": 5280.38, "y": 2337.58, "z": 70.92}, {"x": 5279.83, "y": 2337.45, "z": 70.91}, {"x": 5279.45, "y": 2337.11, "z": 70.9}, {"x": 5278.95, "y": 2336.41, "z": 70.9}, {"x": 5274.25, "y": 2328.25, "z": 71.24}, {"x": 5269.43, "y": 2331.55, "z": 71.14}, {"x": 5272.96, "y": 2337.26, "z": 70.88}, {"x": 5274.72, "y": 2340.05, "z": 70.71}, {"x": 5274.95, "y": 2340.51, "z": 70.7}, {"x": 5274.94, "y": 2340.97, "z": 70.68}, {"x": 5274.82, "y": 2341.43, "z": 70.68}, {"x": 5274.61, "y": 2341.9, "z": 70.69}, {"x": 5274.21, "y": 2342.41, "z": 70.68}, {"x": 5250.55, "y": 2358.63, "z": 69.81}, {"x": 5240.68, "y": 2365.32, "z": 69.5}, {"x": 5240.0, "y": 2365.59, "z": 69.48}, {"x": 5239.44, "y": 2365.66, "z": 69.47}, {"x": 5238.94, "y": 2365.61, "z": 69.46}, {"x": 5238.44, "y": 2365.5, "z": 69.45}, {"x": 5238.03, "y": 2365.31, "z": 69.46}, {"x": 5237.57, "y": 2364.97, "z": 69.46}, {"x": 5237.2, "y": 2364.59, "z": 69.48}, {"x": 5236.83, "y": 2364.17, "z": 69.5}, {"x": 5236.44, "y": 2363.66, "z": 69.51}, {"x": 5219.81, "y": 2336.5, "z": 69.89}, {"x": 5219.71, "y": 2336.17, "z": 69.9}, {"x": 5219.78, "y": 2335.92, "z": 69.9}, {"x": 5220.03, "y": 2335.68, "z": 69.91}, {"x": 5226.56, "y": 2331.45, "z": 70.42}, {"x": 5224.76, "y": 2328.95, "z": 70.43}, {"x": 5218.66, "y": 2332.78, "z": 70.01}, {"x": 5218.35, "y": 2332.87, "z": 70.0}, {"x": 5218.02, "y": 2332.89, "z": 69.98}, {"x": 5217.74, "y": 2332.81, "z": 69.99}, {"x": 5217.42, "y": 2332.57, "z": 70.0}, {"x": 5217.01, "y": 2332.02, "z": 70.01}, {"x": 5190.0, "y": 2287.64, "z": 70.64}, {"x": 5190.0, "y": 2299.06, "z": 70.41}, {"x": 5193.57, "y": 2304.68, "z": 70.33}, {"x": 5209.32, "y": 2330.64, "z": 69.94}, {"x": 5209.6, "y": 2331.26, "z": 69.92}, {"x": 5209.7, "y": 2331.9, "z": 69.9}, {"x": 5209.65, "y": 2332.49, "z": 69.88}, {"x": 5209.5, "y": 2333.02, "z": 69.87}, {"x": 5209.32, "y": 2333.42, "z": 69.86}, {"x": 5209.05, "y": 2333.86, "z": 69.86}, {"x": 5208.63, "y": 2334.28, "z": 69.87}, {"x": 5207.73, "y": 2334.95, "z": 69.87}, {"x": 5177.12, "y": 2352.76, "z": 67.93}, {"x": 5173.1, "y": 2355.08, "z": 67.68}, {"x": 5172.33, "y": 2355.73, "z": 67.58}, {"x": 5171.54, "y": 2356.04, "z": 67.51}, {"x": 5171.04, "y": 2355.85, "z": 67.47}, {"x": 5170.53, "y": 2355.26, "z": 67.48}, {"x": 5161.34, "y": 2340.15, "z": 67.21}, {"x": 5157.57, "y": 2340.14, "z": 67.09}, {"x": 5166.72, "y": 2356.84, "z": 67.23}, {"x": 5167.58, "y": 2358.0, "z": 67.28}, {"x": 5167.61, "y": 2358.58, "z": 67.26}, {"x": 5167.36, "y": 2358.92, "z": 67.24}, {"x": 5166.7, "y": 2359.17, "z": 67.21}, {"x": 5165.75, "y": 2359.65, "z": 67.17}, {"x": 5148.62, "y": 2370.0, "z": 66.08}, {"x": 5160.02, "y": 2370.0, "z": 66.66}, {"x": 5170.43, "y": 2363.81, "z": 67.34}, {"x": 5170.61, "y": 2362.78, "z": 67.36}, {"x": 5170.95, "y": 2362.59, "z": 67.38}, {"x": 5171.4, "y": 2362.4, "z": 67.42}, {"x": 5171.86, "y": 2362.15, "z": 67.45}, {"x": 5191.39, "y": 2349.92, "z": 68.75}, {"x": 5209.53, "y": 2338.85, "z": 69.87}, {"x": 5210.95, "y": 2338.04, "z": 69.9}, {"x": 5211.69, "y": 2337.78, "z": 69.87}, {"x": 5212.5, "y": 2337.77, "z": 69.85}, {"x": 5213.05, "y": 2337.89, "z": 69.85}, {"x": 5213.68, "y": 2338.15, "z": 69.85}, {"x": 5214.18, "y": 2338.64, "z": 69.87}, {"x": 5214.63, "y": 2339.32, "z": 69.86}, {"x": 5232.17, "y": 2367.83, "z": 69.33}, {"x": 5232.45, "y": 2368.38, "z": 69.32}, {"x": 5232.55, "y": 2368.83, "z": 69.31}, {"x": 5232.52, "y": 2369.33, "z": 69.3}, {"x": 5232.37, "y": 2370.06, "z": 69.28}, {"x": 5232.15, "y": 2370.61, "z": 69.25}, {"x": 5231.8, "y": 2371.13, "z": 69.24}, {"x": 5231.31, "y": 2371.7, "z": 69.23}, {"x": 5230.53, "y": 2372.29, "z": 69.2}, {"x": 5220.0, "y": 2379.46, "z": 68.73}, {"x": 5220.0, "y": 2396.9, "z": 68.31}, {"x": 5231.35, "y": 2389.25, "z": 68.83}, {"x": 5231.75, "y": 2389.07, "z": 68.84}, {"x": 5232.33, "y": 2388.89, "z": 68.84}, {"x": 5232.89, "y": 2388.8, "z": 68.85}, {"x": 5233.47, "y": 2388.9, "z": 68.85}, {"x": 5234.03, "y": 2389.11, "z": 68.86}, {"x": 5234.48, "y": 2389.4, "z": 68.87}, {"x": 5234.84, "y": 2389.79, "z": 68.87}, {"x": 5235.23, "y": 2390.32, "z": 68.9}, {"x": 5235.54, "y": 2390.78, "z": 68.92}, {"x": 5235.83, "y": 2391.08, "z": 68.91}, {"x": 5236.09, "y": 2391.15, "z": 68.91}, {"x": 5237.21, "y": 2391.03, "z": 68.94}, {"x": 5237.56, "y": 2391.02, "z": 68.97}, {"x": 5237.79, "y": 2391.11, "z": 68.99}, {"x": 5244.23, "y": 2400.0, "z": 69.18}, {"x": 5251.14, "y": 2400.0, "z": 69.24}, {"x": 5241.35, "y": 2386.45, "z": 69.14}, {"x": 5240.81, "y": 2385.58, "z": 69.12}, {"x": 5240.58, "y": 2385.05, "z": 69.13}, {"x": 5240.48, "y": 2384.4, "z": 69.11}, {"x": 5240.58, "y": 2383.52, "z": 69.12}, {"x": 5240.75, "y": 2383.04, "z": 69.14}, {"x": 5241.07, "y": 2382.56, "z": 69.16}, {"x": 5241.62, "y": 2382.07, "z": 69.22}, {"x": 5250.46, "y": 2376.03, "z": 69.62}, {"x": 5259.13, "y": 2369.98, "z": 69.9}, {"x": 5280.86, "y": 2355.18, "z": 70.65}, {"x": 5313.37, "y": 2332.91, "z": 71.78}, {"x": 5316.35, "y": 2331.01, "z": 71.84}, {"x": 5317.04, "y": 2330.65, "z": 71.84}, {"x": 5317.63, "y": 2330.41, "z": 71.83}, {"x": 5318.37, "y": 2330.4, "z": 71.83}, {"x": 5319.14, "y": 2330.69, "z": 71.84}, {"x": 5319.86, "y": 2331.41, "z": 71.85}, {"x": 5320.99, "y": 2333.77, "z": 71.87}, {"x": 5337.15, "y": 2364.58, "z": 71.71}, {"x": 5338.04, "y": 2366.29, "z": 71.66}, {"x": 5337.97, "y": 2366.9, "z": 71.65}, {"x": 5337.57, "y": 2367.32, "z": 71.66}, {"x": 5335.67, "y": 2368.8, "z": 71.67}, {"x": 5290.94, "y": 2400.19, "z": 70.32}, {"x": 5280.0, "y": 2407.51, "z": 70.05}, {"x": 5280.0, "y": 2413.56, "z": 70.11}, {"x": 5297.38, "y": 2401.89, "z": 70.53}, {"x": 5297.66, "y": 2401.71, "z": 70.53}, {"x": 5297.98, "y": 2401.68, "z": 70.55}, {"x": 5298.33, "y": 2401.69, "z": 70.53}, {"x": 5298.65, "y": 2401.74, "z": 70.51}, {"x": 5299.22, "y": 2402.42, "z": 70.52}, {"x": 5311.88, "y": 2419.73, "z": 70.46}, {"x": 5316.7, "y": 2419.79, "z": 70.48}, {"x": 5302.78, "y": 2400.09, "z": 70.56}, {"x": 5302.04, "y": 2398.98, "z": 70.56}, {"x": 5302.02, "y": 2398.79, "z": 70.56}, {"x": 5302.1, "y": 2398.58, "z": 70.57}, {"x": 5302.24, "y": 2398.41, "z": 70.58}, {"x": 5313.36, "y": 2390.94, "z": 71.07}, {"x": 5313.64, "y": 2390.55, "z": 71.07}, {"x": 5313.95, "y": 2390.16, "z": 71.06}, {"x": 5314.42, "y": 2389.98, "z": 71.08}, {"x": 5314.96, "y": 2389.97, "z": 71.11}, {"x": 5320.84, "y": 2385.94, "z": 71.21}, {"x": 5337.84, "y": 2373.86, "z": 71.66}, {"x": 5340.12, "y": 2372.61, "z": 71.64}, {"x": 5340.91, "y": 2372.57, "z": 71.63}, {"x": 5341.4, "y": 2372.75, "z": 71.61}, {"x": 5342.89, "y": 2375.42, "z": 71.55}, {"x": 5355.85, "y": 2400.0, "z": 71.25}], "id": 1224529}, "1224509": {"area_boundary": [{"x": 5042.53, "y": 2366.6, "z": 60.48}, {"x": 5048.95, "y": 2376.56, "z": 60.7}, {"x": 5049.11, "y": 2376.86, "z": 60.7}, {"x": 5049.08, "y": 2377.14, "z": 60.7}, {"x": 5048.85, "y": 2377.43, "z": 60.71}, {"x": 5041.84, "y": 2381.45, "z": 60.68}, {"x": 5044.24, "y": 2386.2, "z": 60.82}, {"x": 5052.43, "y": 2382.42, "z": 60.93}, {"x": 5052.76, "y": 2382.41, "z": 60.93}, {"x": 5052.98, "y": 2382.56, "z": 60.95}, {"x": 5055.79, "y": 2386.62, "z": 61.16}, {"x": 5057.12, "y": 2388.01, "z": 61.24}, {"x": 5064.28, "y": 2398.88, "z": 61.68}, {"x": 5078.72, "y": 2421.18, "z": 62.1}, {"x": 5080.61, "y": 2424.01, "z": 62.16}, {"x": 5080.68, "y": 2424.34, "z": 62.16}, {"x": 5080.21, "y": 2425.35, "z": 62.14}, {"x": 5079.94, "y": 2425.78, "z": 62.12}, {"x": 5079.7, "y": 2426.08, "z": 62.12}, {"x": 5068.61, "y": 2433.57, "z": 61.51}, {"x": 5063.29, "y": 2437.81, "z": 61.16}, {"x": 5066.87, "y": 2439.45, "z": 61.29}, {"x": 5071.98, "y": 2436.17, "z": 61.59}, {"x": 5089.79, "y": 2424.19, "z": 62.53}, {"x": 5107.22, "y": 2412.35, "z": 63.55}, {"x": 5125.13, "y": 2400.0, "z": 64.58}, {"x": 5117.72, "y": 2400.0, "z": 64.3}, {"x": 5103.53, "y": 2409.67, "z": 63.5}, {"x": 5091.22, "y": 2418.13, "z": 62.78}, {"x": 5087.62, "y": 2420.55, "z": 62.53}, {"x": 5087.08, "y": 2420.93, "z": 62.46}, {"x": 5086.61, "y": 2421.09, "z": 62.42}, {"x": 5086.26, "y": 2421.18, "z": 62.39}, {"x": 5085.84, "y": 2421.16, "z": 62.37}, {"x": 5085.53, "y": 2421.09, "z": 62.35}, {"x": 5085.26, "y": 2420.92, "z": 62.32}, {"x": 5084.98, "y": 2420.46, "z": 62.31}, {"x": 5079.22, "y": 2411.45, "z": 62.14}, {"x": 5061.01, "y": 2383.65, "z": 61.26}, {"x": 5058.92, "y": 2380.54, "z": 61.13}, {"x": 5058.09, "y": 2379.47, "z": 61.07}, {"x": 5056.23, "y": 2376.61, "z": 60.91}, {"x": 5055.49, "y": 2375.57, "z": 60.85}, {"x": 5051.0, "y": 2368.54, "z": 60.62}, {"x": 5047.63, "y": 2363.08, "z": 60.59}], "id": 1224509}, "1224499": {"area_boundary": [{"x": 5220.0, "y": 2448.31, "z": 68.12}, {"x": 5195.83, "y": 2464.77, "z": 66.86}, {"x": 5193.04, "y": 2465.78, "z": 66.8}, {"x": 5188.68, "y": 2468.75, "z": 66.52}, {"x": 5187.57, "y": 2469.36, "z": 66.49}, {"x": 5187.18, "y": 2469.45, "z": 66.48}, {"x": 5186.86, "y": 2469.41, "z": 66.47}, {"x": 5186.63, "y": 2469.25, "z": 66.47}, {"x": 5185.06, "y": 2467.04, "z": 66.43}, {"x": 5183.61, "y": 2464.83, "z": 66.42}, {"x": 5178.04, "y": 2457.02, "z": 66.19}, {"x": 5169.09, "y": 2444.5, "z": 65.98}, {"x": 5165.82, "y": 2440.03, "z": 66.0}, {"x": 5165.28, "y": 2439.13, "z": 65.94}, {"x": 5164.83, "y": 2438.0, "z": 65.96}, {"x": 5164.66, "y": 2437.07, "z": 65.93}, {"x": 5164.72, "y": 2435.69, "z": 65.92}, {"x": 5165.27, "y": 2434.78, "z": 65.99}, {"x": 5166.8, "y": 2433.46, "z": 66.08}, {"x": 5170.51, "y": 2430.82, "z": 66.28}, {"x": 5206.01, "y": 2406.55, "z": 67.74}, {"x": 5215.51, "y": 2399.98, "z": 68.12}, {"x": 5220.0, "y": 2396.9, "z": 68.31}, {"x": 5220.0, "y": 2379.46, "z": 68.73}, {"x": 5200.44, "y": 2392.72, "z": 67.86}, {"x": 5193.52, "y": 2397.52, "z": 67.53}, {"x": 5173.62, "y": 2411.12, "z": 66.71}, {"x": 5156.57, "y": 2422.6, "z": 65.98}, {"x": 5156.03, "y": 2422.83, "z": 65.95}, {"x": 5155.44, "y": 2422.97, "z": 65.92}, {"x": 5154.98, "y": 2422.94, "z": 65.9}, {"x": 5154.43, "y": 2422.77, "z": 65.87}, {"x": 5153.73, "y": 2422.38, "z": 65.83}, {"x": 5153.22, "y": 2422.01, "z": 65.79}, {"x": 5152.41, "y": 2420.98, "z": 65.76}, {"x": 5132.83, "y": 2388.88, "z": 64.96}, {"x": 5132.59, "y": 2388.35, "z": 64.93}, {"x": 5132.4, "y": 2387.73, "z": 64.89}, {"x": 5132.36, "y": 2387.24, "z": 64.88}, {"x": 5132.35, "y": 2386.79, "z": 64.86}, {"x": 5132.51, "y": 2386.28, "z": 64.86}, {"x": 5132.81, "y": 2385.87, "z": 64.87}, {"x": 5133.24, "y": 2385.46, "z": 64.88}, {"x": 5133.78, "y": 2385.1, "z": 64.92}, {"x": 5143.52, "y": 2379.06, "z": 65.53}, {"x": 5150.17, "y": 2376.35, "z": 66.08}, {"x": 5160.02, "y": 2370.0, "z": 66.66}, {"x": 5148.62, "y": 2370.0, "z": 66.08}, {"x": 5132.52, "y": 2379.7, "z": 64.99}, {"x": 5132.28, "y": 2381.0, "z": 64.91}, {"x": 5131.19, "y": 2381.85, "z": 64.82}, {"x": 5129.93, "y": 2382.62, "z": 64.79}, {"x": 5129.66, "y": 2382.68, "z": 64.78}, {"x": 5129.35, "y": 2382.66, "z": 64.78}, {"x": 5129.05, "y": 2382.5, "z": 64.78}, {"x": 5128.73, "y": 2382.21, "z": 64.77}, {"x": 5121.31, "y": 2370.0, "z": 64.41}, {"x": 5111.89, "y": 2370.0, "z": 64.14}, {"x": 5125.93, "y": 2392.88, "z": 64.68}, {"x": 5126.06, "y": 2393.27, "z": 64.68}, {"x": 5126.09, "y": 2393.71, "z": 64.68}, {"x": 5126.07, "y": 2393.94, "z": 64.67}, {"x": 5126.01, "y": 2394.18, "z": 64.67}, {"x": 5125.88, "y": 2394.43, "z": 64.68}, {"x": 5125.67, "y": 2394.65, "z": 64.68}, {"x": 5117.72, "y": 2400.0, "z": 64.3}, {"x": 5125.13, "y": 2400.0, "z": 64.58}, {"x": 5126.69, "y": 2398.93, "z": 64.71}, {"x": 5127.16, "y": 2398.75, "z": 64.72}, {"x": 5127.74, "y": 2398.67, "z": 64.73}, {"x": 5128.25, "y": 2398.63, "z": 64.74}, {"x": 5128.85, "y": 2398.82, "z": 64.77}, {"x": 5129.29, "y": 2399.14, "z": 64.79}, {"x": 5129.76, "y": 2399.54, "z": 64.83}, {"x": 5130.09, "y": 2399.92, "z": 64.84}, {"x": 5145.96, "y": 2425.82, "z": 65.61}, {"x": 5146.29, "y": 2426.52, "z": 65.6}, {"x": 5146.51, "y": 2427.24, "z": 65.6}, {"x": 5146.59, "y": 2427.88, "z": 65.59}, {"x": 5146.53, "y": 2428.81, "z": 65.58}, {"x": 5146.17, "y": 2429.6, "z": 65.57}, {"x": 5145.59, "y": 2430.3, "z": 65.57}, {"x": 5142.08, "y": 2432.68, "z": 65.42}, {"x": 5130.0, "y": 2440.97, "z": 64.85}, {"x": 5130.0, "y": 2463.82, "z": 64.67}, {"x": 5143.84, "y": 2450.6, "z": 65.13}, {"x": 5147.97, "y": 2447.04, "z": 65.29}, {"x": 5154.25, "y": 2442.73, "z": 65.52}, {"x": 5155.75, "y": 2442.49, "z": 65.54}, {"x": 5157.03, "y": 2442.76, "z": 65.56}, {"x": 5158.13, "y": 2443.33, "z": 65.63}, {"x": 5158.73, "y": 2443.96, "z": 65.67}, {"x": 5172.99, "y": 2463.63, "z": 66.18}, {"x": 5182.31, "y": 2476.87, "z": 66.34}, {"x": 5182.67, "y": 2477.52, "z": 66.35}, {"x": 5182.74, "y": 2477.98, "z": 66.34}, {"x": 5182.65, "y": 2478.35, "z": 66.38}, {"x": 5182.35, "y": 2478.83, "z": 66.43}, {"x": 5180.21, "y": 2480.96, "z": 66.64}, {"x": 5183.5, "y": 2485.46, "z": 66.72}, {"x": 5186.27, "y": 2483.84, "z": 66.58}, {"x": 5186.71, "y": 2483.79, "z": 66.54}, {"x": 5187.07, "y": 2483.83, "z": 66.52}, {"x": 5187.33, "y": 2483.95, "z": 66.52}, {"x": 5187.71, "y": 2484.41, "z": 66.53}, {"x": 5191.82, "y": 2490.0, "z": 66.62}, {"x": 5201.69, "y": 2490.0, "z": 66.87}, {"x": 5191.41, "y": 2475.75, "z": 66.57}, {"x": 5191.17, "y": 2475.29, "z": 66.55}, {"x": 5191.11, "y": 2474.94, "z": 66.54}, {"x": 5191.22, "y": 2474.57, "z": 66.55}, {"x": 5191.51, "y": 2474.24, "z": 66.57}, {"x": 5192.73, "y": 2473.28, "z": 66.59}, {"x": 5193.35, "y": 2472.73, "z": 66.62}, {"x": 5220.0, "y": 2454.47, "z": 67.99}], "id": 1224499}, "1224498": {"area_boundary": [{"x": 5104.53, "y": 2488.15, "z": 63.66}, {"x": 5110.51, "y": 2482.47, "z": 63.9}, {"x": 5116.98, "y": 2476.3, "z": 64.13}, {"x": 5113.51, "y": 2473.63, "z": 64.13}, {"x": 5110.03, "y": 2476.77, "z": 63.99}, {"x": 5107.1, "y": 2479.57, "z": 63.95}, {"x": 5101.3, "y": 2485.1, "z": 63.73}, {"x": 5095.12, "y": 2491.1, "z": 63.43}, {"x": 5090.57, "y": 2495.35, "z": 63.27}, {"x": 5093.71, "y": 2498.4, "z": 63.2}], "id": 1224498}, "1224493": {"area_boundary": [{"x": 5355.85, "y": 2400.0, "z": 71.25}, {"x": 5360.17, "y": 2408.6, "z": 71.27}, {"x": 5363.42, "y": 2414.72, "z": 71.24}, {"x": 5369.47, "y": 2426.02, "z": 71.09}, {"x": 5372.23, "y": 2431.62, "z": 71.02}, {"x": 5372.62, "y": 2432.34, "z": 71.0}, {"x": 5372.68, "y": 2432.9, "z": 70.99}, {"x": 5372.44, "y": 2433.28, "z": 70.99}, {"x": 5371.98, "y": 2433.75, "z": 71.0}, {"x": 5370.83, "y": 2434.75, "z": 71.03}, {"x": 5372.65, "y": 2438.8, "z": 71.04}, {"x": 5374.24, "y": 2437.41, "z": 70.99}, {"x": 5374.65, "y": 2437.21, "z": 70.97}, {"x": 5375.16, "y": 2437.29, "z": 70.97}, {"x": 5375.56, "y": 2437.67, "z": 70.98}, {"x": 5381.42, "y": 2448.81, "z": 70.93}, {"x": 5388.94, "y": 2463.05, "z": 70.87}, {"x": 5393.16, "y": 2471.05, "z": 70.81}, {"x": 5394.28, "y": 2473.43, "z": 70.8}, {"x": 5394.45, "y": 2474.03, "z": 70.8}, {"x": 5394.5, "y": 2474.57, "z": 70.79}, {"x": 5394.45, "y": 2475.1, "z": 70.77}, {"x": 5394.26, "y": 2475.68, "z": 70.77}, {"x": 5393.62, "y": 2476.56, "z": 70.78}, {"x": 5391.71, "y": 2478.23, "z": 70.73}, {"x": 5381.97, "y": 2487.32, "z": 70.51}, {"x": 5381.51, "y": 2487.33, "z": 70.51}, {"x": 5381.09, "y": 2487.1, "z": 70.55}, {"x": 5377.74, "y": 2482.89, "z": 70.81}, {"x": 5372.64, "y": 2486.92, "z": 70.67}, {"x": 5377.0, "y": 2490.69, "z": 70.47}, {"x": 5377.24, "y": 2491.27, "z": 70.43}, {"x": 5377.13, "y": 2491.77, "z": 70.38}, {"x": 5370.69, "y": 2497.7, "z": 70.3}, {"x": 5359.62, "y": 2507.96, "z": 70.07}, {"x": 5348.06, "y": 2518.66, "z": 69.88}, {"x": 5343.61, "y": 2522.76, "z": 69.81}, {"x": 5343.03, "y": 2523.17, "z": 69.8}, {"x": 5342.38, "y": 2523.53, "z": 69.78}, {"x": 5341.67, "y": 2523.65, "z": 69.76}, {"x": 5340.95, "y": 2523.64, "z": 69.73}, {"x": 5340.22, "y": 2523.48, "z": 69.72}, {"x": 5339.65, "y": 2523.1, "z": 69.71}, {"x": 5334.4, "y": 2516.27, "z": 69.78}, {"x": 5329.42, "y": 2509.12, "z": 69.83}, {"x": 5317.22, "y": 2491.93, "z": 69.86}, {"x": 5316.87, "y": 2491.45, "z": 69.88}, {"x": 5316.82, "y": 2490.82, "z": 69.89}, {"x": 5317.2, "y": 2490.24, "z": 69.88}, {"x": 5317.95, "y": 2489.49, "z": 69.9}, {"x": 5319.25, "y": 2488.26, "z": 69.94}, {"x": 5316.71, "y": 2484.89, "z": 69.92}, {"x": 5314.91, "y": 2486.55, "z": 69.88}, {"x": 5314.52, "y": 2486.78, "z": 69.88}, {"x": 5314.29, "y": 2486.84, "z": 69.88}, {"x": 5313.85, "y": 2486.84, "z": 69.87}, {"x": 5313.56, "y": 2486.72, "z": 69.88}, {"x": 5313.18, "y": 2486.36, "z": 69.89}, {"x": 5312.85, "y": 2486.0, "z": 69.9}, {"x": 5267.71, "y": 2423.07, "z": 69.62}, {"x": 5267.47, "y": 2422.59, "z": 69.58}, {"x": 5267.45, "y": 2422.32, "z": 69.57}, {"x": 5267.5, "y": 2422.05, "z": 69.57}, {"x": 5267.68, "y": 2421.81, "z": 69.57}, {"x": 5270.3, "y": 2420.15, "z": 69.72}, {"x": 5280.0, "y": 2413.56, "z": 70.11}, {"x": 5280.0, "y": 2407.51, "z": 70.05}, {"x": 5267.88, "y": 2415.46, "z": 69.69}, {"x": 5265.17, "y": 2416.82, "z": 69.56}, {"x": 5264.52, "y": 2417.22, "z": 69.51}, {"x": 5263.9, "y": 2417.28, "z": 69.5}, {"x": 5263.44, "y": 2417.08, "z": 69.48}, {"x": 5263.07, "y": 2416.65, "z": 69.49}, {"x": 5251.14, "y": 2400.0, "z": 69.24}, {"x": 5244.23, "y": 2400.0, "z": 69.18}, {"x": 5247.68, "y": 2404.74, "z": 69.27}, {"x": 5246.27, "y": 2405.95, "z": 69.3}, {"x": 5256.62, "y": 2420.31, "z": 69.51}, {"x": 5257.0, "y": 2420.85, "z": 69.5}, {"x": 5257.08, "y": 2421.48, "z": 69.49}, {"x": 5256.89, "y": 2422.04, "z": 69.49}, {"x": 5256.53, "y": 2422.43, "z": 69.5}, {"x": 5255.68, "y": 2423.01, "z": 69.51}, {"x": 5256.0, "y": 2423.6, "z": 69.51}, {"x": 5255.23, "y": 2424.09, "z": 69.53}, {"x": 5254.78, "y": 2423.46, "z": 69.54}, {"x": 5229.07, "y": 2441.71, "z": 68.45}, {"x": 5220.0, "y": 2448.31, "z": 68.12}, {"x": 5220.0, "y": 2454.47, "z": 67.99}, {"x": 5223.55, "y": 2452.05, "z": 68.17}, {"x": 5223.78, "y": 2452.02, "z": 68.19}, {"x": 5224.07, "y": 2452.09, "z": 68.19}, {"x": 5224.37, "y": 2452.3, "z": 68.21}, {"x": 5228.43, "y": 2458.41, "z": 68.39}, {"x": 5232.62, "y": 2455.82, "z": 68.4}, {"x": 5228.19, "y": 2449.59, "z": 68.32}, {"x": 5228.09, "y": 2449.26, "z": 68.32}, {"x": 5228.11, "y": 2449.03, "z": 68.32}, {"x": 5228.26, "y": 2448.83, "z": 68.32}, {"x": 5241.14, "y": 2440.02, "z": 68.87}, {"x": 5257.81, "y": 2428.43, "z": 69.61}, {"x": 5259.2, "y": 2427.52, "z": 69.59}, {"x": 5260.44, "y": 2426.68, "z": 69.53}, {"x": 5260.75, "y": 2426.65, "z": 69.54}, {"x": 5261.19, "y": 2426.88, "z": 69.56}, {"x": 5261.48, "y": 2427.25, "z": 69.57}, {"x": 5273.93, "y": 2444.61, "z": 69.86}, {"x": 5273.93, "y": 2444.96, "z": 69.87}, {"x": 5269.26, "y": 2448.15, "z": 69.95}, {"x": 5272.94, "y": 2453.58, "z": 69.91}, {"x": 5277.82, "y": 2450.39, "z": 69.96}, {"x": 5278.09, "y": 2450.44, "z": 69.96}, {"x": 5306.82, "y": 2490.42, "z": 69.87}, {"x": 5307.18, "y": 2490.92, "z": 69.87}, {"x": 5307.34, "y": 2491.31, "z": 69.87}, {"x": 5307.4, "y": 2491.66, "z": 69.88}, {"x": 5307.33, "y": 2492.09, "z": 69.9}, {"x": 5307.08, "y": 2492.58, "z": 69.91}, {"x": 5306.43, "y": 2493.36, "z": 69.95}, {"x": 5291.89, "y": 2506.64, "z": 70.01}, {"x": 5280.0, "y": 2518.26, "z": 69.7}, {"x": 5280.0, "y": 2525.9, "z": 69.68}, {"x": 5309.49, "y": 2498.45, "z": 69.96}, {"x": 5310.15, "y": 2497.99, "z": 69.93}, {"x": 5310.69, "y": 2497.75, "z": 69.92}, {"x": 5311.29, "y": 2497.7, "z": 69.91}, {"x": 5311.94, "y": 2497.86, "z": 69.89}, {"x": 5312.36, "y": 2498.15, "z": 69.89}, {"x": 5312.72, "y": 2498.56, "z": 69.89}, {"x": 5323.83, "y": 2514.0, "z": 69.8}, {"x": 5332.91, "y": 2526.68, "z": 69.74}, {"x": 5334.22, "y": 2528.39, "z": 69.64}, {"x": 5334.4, "y": 2529.2, "z": 69.63}, {"x": 5334.46, "y": 2530.05, "z": 69.65}, {"x": 5334.34, "y": 2530.78, "z": 69.66}, {"x": 5334.15, "y": 2531.38, "z": 69.67}, {"x": 5331.75, "y": 2533.79, "z": 69.66}, {"x": 5317.18, "y": 2547.3, "z": 69.44}, {"x": 5310.0, "y": 2553.85, "z": 69.32}, {"x": 5310.0, "y": 2564.81, "z": 69.37}, {"x": 5321.11, "y": 2554.59, "z": 69.49}, {"x": 5322.9, "y": 2553.07, "z": 69.48}, {"x": 5323.55, "y": 2552.73, "z": 69.48}, {"x": 5324.25, "y": 2552.65, "z": 69.5}, {"x": 5324.87, "y": 2552.8, "z": 69.55}, {"x": 5325.44, "y": 2553.1, "z": 69.58}, {"x": 5325.96, "y": 2553.68, "z": 69.6}, {"x": 5350.83, "y": 2580.0, "z": 69.59}, {"x": 5356.31, "y": 2580.0, "z": 69.61}, {"x": 5346.15, "y": 2569.22, "z": 69.55}, {"x": 5345.83, "y": 2568.77, "z": 69.54}, {"x": 5345.68, "y": 2568.28, "z": 69.53}, {"x": 5345.64, "y": 2567.72, "z": 69.52}, {"x": 5345.68, "y": 2567.22, "z": 69.52}, {"x": 5345.88, "y": 2566.8, "z": 69.53}, {"x": 5346.4, "y": 2566.39, "z": 69.51}, {"x": 5370.0, "y": 2555.95, "z": 69.93}, {"x": 5370.0, "y": 2551.54, "z": 69.88}, {"x": 5360.46, "y": 2555.88, "z": 69.74}, {"x": 5351.67, "y": 2560.2, "z": 69.54}, {"x": 5344.38, "y": 2564.1, "z": 69.49}, {"x": 5343.75, "y": 2564.33, "z": 69.51}, {"x": 5343.13, "y": 2564.47, "z": 69.53}, {"x": 5342.48, "y": 2564.47, "z": 69.54}, {"x": 5341.98, "y": 2564.36, "z": 69.57}, {"x": 5341.59, "y": 2564.12, "z": 69.55}, {"x": 5329.06, "y": 2550.7, "z": 69.58}, {"x": 5328.74, "y": 2550.18, "z": 69.6}, {"x": 5328.48, "y": 2549.63, "z": 69.59}, {"x": 5328.29, "y": 2549.15, "z": 69.57}, {"x": 5328.26, "y": 2548.56, "z": 69.55}, {"x": 5328.46, "y": 2547.99, "z": 69.55}, {"x": 5328.81, "y": 2547.5, "z": 69.56}, {"x": 5334.43, "y": 2542.36, "z": 69.65}, {"x": 5348.27, "y": 2529.54, "z": 69.87}, {"x": 5362.36, "y": 2516.42, "z": 70.14}, {"x": 5368.75, "y": 2510.66, "z": 70.23}, {"x": 5369.4, "y": 2510.51, "z": 70.23}, {"x": 5370.1, "y": 2510.49, "z": 70.23}, {"x": 5370.7, "y": 2510.76, "z": 70.26}, {"x": 5371.09, "y": 2511.16, "z": 70.28}, {"x": 5371.43, "y": 2511.56, "z": 70.3}, {"x": 5373.21, "y": 2515.71, "z": 70.31}, {"x": 5375.04, "y": 2520.0, "z": 70.31}, {"x": 5379.62, "y": 2520.0, "z": 70.28}, {"x": 5374.85, "y": 2508.68, "z": 70.33}, {"x": 5374.05, "y": 2507.53, "z": 70.28}, {"x": 5373.89, "y": 2506.89, "z": 70.28}, {"x": 5374.03, "y": 2506.09, "z": 70.3}, {"x": 5374.36, "y": 2505.45, "z": 70.33}, {"x": 5374.87, "y": 2504.87, "z": 70.39}, {"x": 5385.15, "y": 2495.42, "z": 70.54}, {"x": 5387.59, "y": 2493.15, "z": 70.61}, {"x": 5388.61, "y": 2492.14, "z": 70.63}, {"x": 5395.95, "y": 2485.31, "z": 70.77}, {"x": 5397.0, "y": 2484.67, "z": 70.82}, {"x": 5397.71, "y": 2484.46, "z": 70.8}, {"x": 5398.33, "y": 2484.45, "z": 70.79}, {"x": 5399.0, "y": 2484.52, "z": 70.8}, {"x": 5399.63, "y": 2484.77, "z": 70.81}, {"x": 5400.32, "y": 2485.27, "z": 70.84}, {"x": 5400.72, "y": 2485.72, "z": 70.86}, {"x": 5402.28, "y": 2488.82, "z": 70.9}, {"x": 5408.84, "y": 2502.95, "z": 70.76}, {"x": 5415.47, "y": 2517.1, "z": 70.76}, {"x": 5416.83, "y": 2520.0, "z": 70.71}, {"x": 5425.39, "y": 2520.0, "z": 70.82}, {"x": 5424.9, "y": 2518.75, "z": 70.83}, {"x": 5420.24, "y": 2508.82, "z": 70.86}, {"x": 5414.09, "y": 2495.61, "z": 70.87}, {"x": 5408.39, "y": 2483.25, "z": 70.97}, {"x": 5406.66, "y": 2479.59, "z": 70.96}, {"x": 5406.61, "y": 2479.17, "z": 70.96}, {"x": 5406.57, "y": 2478.54, "z": 70.95}, {"x": 5406.6, "y": 2477.93, "z": 70.95}, {"x": 5406.81, "y": 2477.48, "z": 70.96}, {"x": 5407.18, "y": 2476.95, "z": 70.96}, {"x": 5415.74, "y": 2468.82, "z": 71.24}, {"x": 5429.01, "y": 2456.49, "z": 71.61}, {"x": 5431.16, "y": 2454.5, "z": 71.66}, {"x": 5431.73, "y": 2454.26, "z": 71.68}, {"x": 5432.37, "y": 2454.23, "z": 71.72}, {"x": 5433.28, "y": 2454.67, "z": 71.8}, {"x": 5434.31, "y": 2455.54, "z": 71.89}, {"x": 5435.79, "y": 2458.03, "z": 71.97}, {"x": 5437.26, "y": 2460.85, "z": 72.09}, {"x": 5444.31, "y": 2456.64, "z": 72.31}, {"x": 5442.01, "y": 2453.09, "z": 72.22}, {"x": 5439.64, "y": 2449.36, "z": 72.01}, {"x": 5439.3, "y": 2448.24, "z": 71.95}, {"x": 5439.22, "y": 2447.44, "z": 71.91}, {"x": 5439.37, "y": 2446.89, "z": 71.9}, {"x": 5447.48, "y": 2439.38, "z": 72.17}, {"x": 5460.0, "y": 2427.65, "z": 72.55}, {"x": 5460.0, "y": 2413.0, "z": 72.55}, {"x": 5451.32, "y": 2420.98, "z": 72.28}, {"x": 5438.82, "y": 2432.66, "z": 71.93}, {"x": 5438.44, "y": 2432.89, "z": 71.9}, {"x": 5437.91, "y": 2433.03, "z": 71.89}, {"x": 5437.33, "y": 2433.03, "z": 71.88}, {"x": 5436.8, "y": 2432.81, "z": 71.89}, {"x": 5436.4, "y": 2432.47, "z": 71.9}, {"x": 5435.27, "y": 2430.69, "z": 71.97}, {"x": 5429.91, "y": 2420.0, "z": 72.37}, {"x": 5429.84, "y": 2419.74, "z": 72.37}, {"x": 5429.94, "y": 2419.47, "z": 72.39}, {"x": 5430.33, "y": 2418.97, "z": 72.42}, {"x": 5427.64, "y": 2414.21, "z": 72.45}, {"x": 5427.46, "y": 2413.74, "z": 72.46}, {"x": 5427.51, "y": 2413.25, "z": 72.47}, {"x": 5427.43, "y": 2412.79, "z": 72.48}, {"x": 5417.43, "y": 2393.67, "z": 72.42}, {"x": 5416.4, "y": 2393.79, "z": 72.4}, {"x": 5415.96, "y": 2393.61, "z": 72.4}, {"x": 5415.66, "y": 2393.16, "z": 72.41}, {"x": 5415.68, "y": 2392.65, "z": 72.42}, {"x": 5416.29, "y": 2391.61, "z": 72.42}, {"x": 5405.19, "y": 2370.0, "z": 72.21}, {"x": 5398.68, "y": 2370.0, "z": 72.23}, {"x": 5412.72, "y": 2398.65, "z": 72.31}, {"x": 5412.96, "y": 2398.93, "z": 72.32}, {"x": 5413.35, "y": 2399.27, "z": 72.33}, {"x": 5414.51, "y": 2400.14, "z": 72.39}, {"x": 5414.83, "y": 2400.42, "z": 72.41}, {"x": 5415.01, "y": 2400.68, "z": 72.43}, {"x": 5429.81, "y": 2429.05, "z": 71.98}, {"x": 5433.39, "y": 2435.75, "z": 71.85}, {"x": 5433.57, "y": 2436.33, "z": 71.83}, {"x": 5433.61, "y": 2436.95, "z": 71.81}, {"x": 5433.42, "y": 2437.47, "z": 71.81}, {"x": 5432.99, "y": 2438.16, "z": 71.79}, {"x": 5429.12, "y": 2441.83, "z": 71.7}, {"x": 5416.39, "y": 2453.73, "z": 71.32}, {"x": 5405.1, "y": 2464.17, "z": 71.01}, {"x": 5403.52, "y": 2465.64, "z": 70.99}, {"x": 5402.72, "y": 2466.1, "z": 70.96}, {"x": 5401.92, "y": 2466.32, "z": 70.95}, {"x": 5401.1, "y": 2466.31, "z": 70.92}, {"x": 5400.25, "y": 2466.06, "z": 70.89}, {"x": 5399.54, "y": 2465.57, "z": 70.9}, {"x": 5398.7, "y": 2464.45, "z": 70.94}, {"x": 5397.99, "y": 2463.21, "z": 70.92}, {"x": 5397.92, "y": 2462.5, "z": 70.98}, {"x": 5398.21, "y": 2461.98, "z": 71.05}, {"x": 5400.46, "y": 2460.77, "z": 71.21}, {"x": 5397.76, "y": 2455.23, "z": 71.28}, {"x": 5395.77, "y": 2456.31, "z": 71.12}, {"x": 5395.19, "y": 2456.62, "z": 71.02}, {"x": 5394.69, "y": 2456.63, "z": 70.97}, {"x": 5394.26, "y": 2456.33, "z": 70.94}, {"x": 5393.0, "y": 2454.01, "z": 70.98}, {"x": 5392.42, "y": 2453.12, "z": 70.98}, {"x": 5391.96, "y": 2452.29, "z": 70.96}, {"x": 5385.03, "y": 2438.94, "z": 70.98}, {"x": 5380.0, "y": 2429.16, "z": 71.06}, {"x": 5374.81, "y": 2419.22, "z": 71.14}, {"x": 5370.76, "y": 2411.41, "z": 71.18}, {"x": 5367.13, "y": 2404.45, "z": 71.24}, {"x": 5364.74, "y": 2400.0, "z": 71.28}], "id": 1224493}}} \ No newline at end of file diff --git a/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/sensors/lidar/315966265259836000.feather b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/sensors/lidar/315966265259836000.feather new file mode 100644 index 00000000..f5c2df94 Binary files /dev/null and b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/sensors/lidar/315966265259836000.feather differ diff --git a/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/sensors/lidar/315966265360032000.feather b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/sensors/lidar/315966265360032000.feather new file mode 100644 index 00000000..f950a7ba Binary files /dev/null and b/tests/unit/test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede/sensors/lidar/315966265360032000.feather differ diff --git a/tests/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/annotations.feather b/tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/annotations.feather similarity index 100% rename from tests/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/annotations.feather rename to tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/annotations.feather diff --git a/tests/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/city_SE3_egovehicle.feather b/tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/city_SE3_egovehicle.feather similarity index 100% rename from tests/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/city_SE3_egovehicle.feather rename to tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/city_SE3_egovehicle.feather diff --git a/tests/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76___img_Sim2_city.json b/tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76___img_Sim2_city.json similarity index 100% rename from tests/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76___img_Sim2_city.json rename to tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76___img_Sim2_city.json diff --git a/tests/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76_ground_height_surface____PIT.npy b/tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76_ground_height_surface____PIT.npy similarity index 100% rename from tests/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76_ground_height_surface____PIT.npy rename to tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76_ground_height_surface____PIT.npy diff --git a/tests/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/log_map_archive_adcf7d18-0510-35b0-a2fa-b4cea13a6d76____PIT_city_57819.json b/tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/log_map_archive_adcf7d18-0510-35b0-a2fa-b4cea13a6d76____PIT_city_57819.json similarity index 100% rename from tests/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/log_map_archive_adcf7d18-0510-35b0-a2fa-b4cea13a6d76____PIT_city_57819.json rename to tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/log_map_archive_adcf7d18-0510-35b0-a2fa-b4cea13a6d76____PIT_city_57819.json diff --git a/tests/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/sensors/lidar/315973157959879000.feather b/tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/sensors/lidar/315973157959879000.feather similarity index 100% rename from tests/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/sensors/lidar/315973157959879000.feather rename to tests/unit/test_data/sensor_dataset_logs/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/sensors/lidar/315973157959879000.feather diff --git a/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/annotations.feather b/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/annotations.feather new file mode 100644 index 00000000..4ed097ba Binary files /dev/null and b/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/annotations.feather differ diff --git a/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/city_SE3_egovehicle.feather b/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/city_SE3_egovehicle.feather new file mode 100644 index 00000000..77150261 Binary files /dev/null and b/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/city_SE3_egovehicle.feather differ diff --git a/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76___img_Sim2_city.json b/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76___img_Sim2_city.json new file mode 100644 index 00000000..a660a58f --- /dev/null +++ b/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76___img_Sim2_city.json @@ -0,0 +1 @@ +{"R": [1.0, 0.0, 0.0, 1.0], "t": [-1368.60009765625, -111.300048828125], "s": 3.3333333333333335} \ No newline at end of file diff --git a/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76_ground_height_surface____PIT.npy b/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76_ground_height_surface____PIT.npy new file mode 100644 index 00000000..619deacc Binary files /dev/null and b/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/adcf7d18-0510-35b0-a2fa-b4cea13a6d76_ground_height_surface____PIT.npy differ diff --git a/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/log_map_archive_adcf7d18-0510-35b0-a2fa-b4cea13a6d76____PIT_city_57819.json b/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/log_map_archive_adcf7d18-0510-35b0-a2fa-b4cea13a6d76____PIT_city_57819.json new file mode 100644 index 00000000..07f32e85 --- /dev/null +++ b/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/map/log_map_archive_adcf7d18-0510-35b0-a2fa-b4cea13a6d76____PIT_city_57819.json @@ -0,0 +1 @@ +{"pedestrian_crossings": {"2643214": {"edge1": [{"x": 1388.19, "y": 197.09, "z": 13.04}, {"x": 1395.07, "y": 176.68, "z": 13.32}], "edge2": [{"x": 1393.3, "y": 198.88, "z": 13.0}, {"x": 1400.15, "y": 180.6, "z": 13.25}], "id": 2643214}, "2643212": {"edge1": [{"x": 1393.36, "y": 174.82, "z": 13.32}, {"x": 1375.46, "y": 168.2, "z": 13.5}], "edge2": [{"x": 1399.76, "y": 181.51, "z": 13.14}, {"x": 1373.06, "y": 171.83, "z": 13.18}], "id": 2643212}, "2643193": {"edge1": [{"x": 1483.64, "y": 231.61, "z": 12.55}, {"x": 1490.02, "y": 212.91, "z": 12.83}], "edge2": [{"x": 1486.93, "y": 233.96, "z": 12.35}, {"x": 1494.92, "y": 211.42, "z": 12.75}], "id": 2643193}, "2643082": {"edge1": [{"x": 1453.19, "y": 315.41, "z": 11.36}, {"x": 1456.77, "y": 305.1, "z": 11.57}], "edge2": [{"x": 1455.18, "y": 319.95, "z": 11.24}, {"x": 1461.26, "y": 303.37, "z": 11.47}], "id": 2643082}, "2642619": {"edge1": [{"x": 1499.18, "y": 239.89, "z": 12.26}, {"x": 1508.42, "y": 215.34, "z": 12.73}], "edge2": [{"x": 1504.81, "y": 237.68, "z": 12.25}, {"x": 1511.51, "y": 219.52, "z": 12.73}], "id": 2642619}, "2642618": {"edge1": [{"x": 1499.87, "y": 239.59, "z": 12.28}, {"x": 1486.15, "y": 234.29, "z": 12.44}], "edge2": [{"x": 1502.44, "y": 236.29, "z": 12.19}, {"x": 1485.98, "y": 230.87, "z": 12.41}], "id": 2642618}, "2642760": {"edge1": [{"x": 1474.37, "y": 308.64, "z": 11.35}, {"x": 1461.02, "y": 303.63, "z": 11.45}], "edge2": [{"x": 1476.41, "y": 312.39, "z": 11.33}, {"x": 1457.83, "y": 305.78, "z": 11.47}], "id": 2642760}, "2642756": {"edge1": [{"x": 1466.23, "y": 330.45, "z": 11.04}, {"x": 1474.36, "y": 307.96, "z": 11.36}], "edge2": [{"x": 1471.78, "y": 325.76, "z": 11.18}, {"x": 1476.34, "y": 312.17, "z": 11.34}], "id": 2642756}, "2642718": {"edge1": [{"x": 1511.58, "y": 216.83, "z": 12.79}, {"x": 1493.0, "y": 210.81, "z": 12.86}], "edge2": [{"x": 1511.27, "y": 220.75, "z": 12.63}, {"x": 1490.57, "y": 213.64, "z": 12.72}], "id": 2642718}, "2641731": {"edge1": [{"x": 1598.97, "y": 231.62, "z": 13.16}, {"x": 1619.13, "y": 221.78, "z": 13.46}], "edge2": [{"x": 1600.38, "y": 236.09, "z": 12.87}, {"x": 1621.7, "y": 222.16, "z": 13.38}], "id": 2641731}, "2641729": {"edge1": [{"x": 1617.04, "y": 265.49, "z": 12.23}, {"x": 1599.01, "y": 230.17, "z": 13.15}], "edge2": [{"x": 1610.92, "y": 263.91, "z": 12.39}, {"x": 1598.16, "y": 237.15, "z": 13.07}], "id": 2641729}}, "lane_segments": {"42806288": {"id": 42806288, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1502.42, "y": 210.24, "z": 12.7}, {"x": 1495.61, "y": 239.02, "z": 12.19}, {"x": 1495.48, "y": 239.66, "z": 12.18}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1508.47, "y": 212.44, "z": 12.71}, {"x": 1498.46, "y": 239.86, "z": 12.18}], "right_lane_mark_type": "NONE", "successors": [42811961], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806291": {"id": 42806291, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1397.99, "y": 191.56, "z": 13.03}, {"x": 1366.83, "y": 180.53, "z": 13.18}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1396.68, "y": 194.8, "z": 12.93}, {"x": 1365.69, "y": 183.86, "z": 13.07}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42808599], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42809419}, "42806293": {"id": 42806293, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1478.91, "y": 275.45, "z": 11.75}, {"x": 1480.35, "y": 271.36, "z": 11.81}, {"x": 1480.39, "y": 270.1, "z": 11.82}, {"x": 1480.07, "y": 268.71, "z": 11.83}, {"x": 1479.21, "y": 267.77, "z": 11.83}, {"x": 1478.07, "y": 267.16, "z": 11.83}, {"x": 1476.82, "y": 266.76, "z": 11.82}, {"x": 1473.35, "y": 265.59, "z": 11.89}, {"x": 1473.27, "y": 265.58, "z": 11.89}, {"x": 1471.19, "y": 264.87, "z": 11.98}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1472.54, "y": 273.34, "z": 11.78}, {"x": 1472.71, "y": 272.67, "z": 11.78}, {"x": 1472.62, "y": 272.19, "z": 11.8}, {"x": 1472.67, "y": 271.37, "z": 11.79}, {"x": 1471.99, "y": 270.96, "z": 11.82}, {"x": 1470.27, "y": 270.41, "z": 11.9}, {"x": 1469.26, "y": 270.09, "z": 11.95}], "right_lane_mark_type": "NONE", "successors": [42806482], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806338": {"id": 42806338, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1459.23, "y": 329.93, "z": 10.98}, {"x": 1469.41, "y": 302.25, "z": 11.44}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1456.07, "y": 328.82, "z": 10.93}, {"x": 1459.79, "y": 317.49, "z": 11.31}, {"x": 1462.92, "y": 300.07, "z": 11.41}], "right_lane_mark_type": "NONE", "successors": [42809705], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42808643}, "42806420": {"id": 42806420, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1509.06, "y": 231.1, "z": 12.57}, {"x": 1484.88, "y": 222.63, "z": 12.67}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1507.87, "y": 234.12, "z": 12.39}, {"x": 1483.78, "y": 225.64, "z": 12.53}], "right_lane_mark_type": "NONE", "successors": [42807335], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806422": {"id": 42806422, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1487.16, "y": 216.4, "z": 12.69}, {"x": 1490.13, "y": 217.44, "z": 12.67}, {"x": 1491.55, "y": 217.69, "z": 12.66}, {"x": 1492.77, "y": 217.8, "z": 12.66}, {"x": 1494.0, "y": 217.79, "z": 12.65}, {"x": 1495.25, "y": 217.53, "z": 12.64}, {"x": 1496.57, "y": 217.14, "z": 12.63}, {"x": 1497.74, "y": 216.74, "z": 12.62}, {"x": 1498.57, "y": 216.34, "z": 12.61}, {"x": 1499.42, "y": 215.82, "z": 12.6}, {"x": 1500.18, "y": 215.12, "z": 12.61}, {"x": 1500.71, "y": 214.3, "z": 12.62}, {"x": 1501.13, "y": 213.54, "z": 12.64}, {"x": 1501.83, "y": 211.78, "z": 12.67}, {"x": 1502.42, "y": 210.24, "z": 12.7}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1488.27, "y": 213.4, "z": 12.68}, {"x": 1491.2, "y": 214.29, "z": 12.66}, {"x": 1492.45, "y": 214.45, "z": 12.64}, {"x": 1493.45, "y": 214.4, "z": 12.64}, {"x": 1494.19, "y": 213.87, "z": 12.63}, {"x": 1494.75, "y": 213.21, "z": 12.65}, {"x": 1496.78, "y": 208.12, "z": 12.74}], "right_lane_mark_type": "NONE", "successors": [42811329], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806482": {"id": 42806482, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1471.19, "y": 264.87, "z": 11.98}, {"x": 1440.0, "y": 254.41, "z": 12.34}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1469.26, "y": 270.09, "z": 11.95}, {"x": 1440.0, "y": 259.66, "z": 12.34}], "right_lane_mark_type": "NONE", "successors": [42844999], "predecessors": [42806293], "right_neighbor_id": null, "left_neighbor_id": null}, "42806507": {"id": 42806507, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1457.25, "y": 212.64, "z": 12.82}, {"x": 1442.01, "y": 207.33, "z": 12.9}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1456.21, "y": 215.73, "z": 12.69}, {"x": 1441.06, "y": 210.32, "z": 12.77}], "right_lane_mark_type": "NONE", "successors": [42810779], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806529": {"id": 42806529, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1386.01, "y": 167.54, "z": 13.52}, {"x": 1390.34, "y": 178.65, "z": 13.17}, {"x": 1391.61, "y": 181.6, "z": 13.14}, {"x": 1392.63, "y": 183.46, "z": 13.11}, {"x": 1393.45, "y": 184.8, "z": 13.1}, {"x": 1394.27, "y": 185.97, "z": 13.09}, {"x": 1395.08, "y": 186.73, "z": 13.09}, {"x": 1396.73, "y": 187.69, "z": 13.08}, {"x": 1398.98, "y": 188.67, "z": 13.09}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1389.06, "y": 166.15, "z": 13.57}, {"x": 1394.05, "y": 177.94, "z": 13.18}, {"x": 1394.64, "y": 179.24, "z": 13.14}, {"x": 1395.22, "y": 180.56, "z": 13.12}, {"x": 1395.91, "y": 181.7, "z": 13.1}, {"x": 1396.71, "y": 182.93, "z": 13.09}, {"x": 1397.71, "y": 183.82, "z": 13.08}, {"x": 1398.89, "y": 184.58, "z": 13.07}, {"x": 1400.28, "y": 185.29, "z": 13.07}], "right_lane_mark_type": "NONE", "successors": [42810791], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806535": {"id": 42806535, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1382.75, "y": 169.07, "z": 13.47}, {"x": 1383.58, "y": 172.8, "z": 13.24}, {"x": 1383.39, "y": 174.08, "z": 13.19}, {"x": 1383.12, "y": 174.98, "z": 13.17}, {"x": 1382.71, "y": 175.73, "z": 13.17}, {"x": 1381.96, "y": 176.54, "z": 13.17}, {"x": 1380.54, "y": 177.55, "z": 13.17}, {"x": 1378.5, "y": 178.26, "z": 13.17}, {"x": 1376.49, "y": 178.52, "z": 13.17}, {"x": 1374.25, "y": 178.58, "z": 13.18}, {"x": 1372.39, "y": 178.47, "z": 13.19}, {"x": 1370.3, "y": 178.21, "z": 13.19}, {"x": 1367.83, "y": 177.56, "z": 13.22}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1386.01, "y": 167.54, "z": 13.52}, {"x": 1387.72, "y": 172.21, "z": 13.39}, {"x": 1387.77, "y": 172.92, "z": 13.36}, {"x": 1387.72, "y": 173.67, "z": 13.33}, {"x": 1387.53, "y": 174.89, "z": 13.28}, {"x": 1387.04, "y": 176.41, "z": 13.21}, {"x": 1385.99, "y": 178.16, "z": 13.18}, {"x": 1384.69, "y": 179.43, "z": 13.16}, {"x": 1383.33, "y": 180.55, "z": 13.15}, {"x": 1382.32, "y": 181.24, "z": 13.16}, {"x": 1381.07, "y": 181.67, "z": 13.17}, {"x": 1379.28, "y": 182.11, "z": 13.17}, {"x": 1377.21, "y": 182.38, "z": 13.17}, {"x": 1375.22, "y": 182.56, "z": 13.15}, {"x": 1374.33, "y": 182.53, "z": 13.15}, {"x": 1373.23, "y": 182.47, "z": 13.14}, {"x": 1371.27, "y": 182.02, "z": 13.14}, {"x": 1366.83, "y": 180.53, "z": 13.18}], "right_lane_mark_type": "NONE", "successors": [42808600], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806677": {"id": 42806677, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1502.42, "y": 210.24, "z": 12.7}, {"x": 1501.9, "y": 211.68, "z": 12.67}, {"x": 1501.59, "y": 212.57, "z": 12.66}, {"x": 1501.24, "y": 213.46, "z": 12.64}, {"x": 1500.73, "y": 214.44, "z": 12.62}, {"x": 1500.25, "y": 215.14, "z": 12.61}, {"x": 1499.58, "y": 215.85, "z": 12.6}, {"x": 1498.6, "y": 216.62, "z": 12.61}, {"x": 1497.66, "y": 217.35, "z": 12.62}, {"x": 1496.76, "y": 218.02, "z": 12.63}, {"x": 1495.94, "y": 218.54, "z": 12.65}, {"x": 1495.09, "y": 219.15, "z": 12.65}, {"x": 1494.22, "y": 219.79, "z": 12.66}, {"x": 1493.3, "y": 220.45, "z": 12.67}, {"x": 1492.38, "y": 221.14, "z": 12.68}, {"x": 1491.39, "y": 221.77, "z": 12.7}, {"x": 1490.33, "y": 222.34, "z": 12.7}, {"x": 1489.45, "y": 222.81, "z": 12.7}, {"x": 1488.54, "y": 223.06, "z": 12.68}, {"x": 1487.68, "y": 223.18, "z": 12.67}, {"x": 1486.67, "y": 223.1, "z": 12.66}, {"x": 1485.75, "y": 222.9, "z": 12.66}, {"x": 1484.88, "y": 222.63, "z": 12.67}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1508.47, "y": 212.44, "z": 12.71}, {"x": 1508.16, "y": 213.19, "z": 12.68}, {"x": 1507.74, "y": 213.87, "z": 12.65}, {"x": 1507.27, "y": 214.67, "z": 12.64}, {"x": 1506.42, "y": 215.78, "z": 12.61}, {"x": 1505.54, "y": 216.66, "z": 12.6}, {"x": 1504.72, "y": 217.31, "z": 12.59}, {"x": 1504.02, "y": 217.9, "z": 12.58}, {"x": 1503.17, "y": 218.63, "z": 12.58}, {"x": 1502.27, "y": 219.41, "z": 12.59}, {"x": 1501.27, "y": 220.11, "z": 12.59}, {"x": 1500.21, "y": 220.86, "z": 12.61}, {"x": 1499.09, "y": 221.67, "z": 12.62}, {"x": 1498.09, "y": 222.32, "z": 12.64}, {"x": 1497.21, "y": 222.87, "z": 12.66}, {"x": 1496.3, "y": 223.39, "z": 12.67}, {"x": 1495.21, "y": 224.06, "z": 12.69}, {"x": 1494.1, "y": 224.72, "z": 12.69}, {"x": 1493.07, "y": 225.17, "z": 12.68}, {"x": 1492.03, "y": 225.62, "z": 12.63}, {"x": 1491.06, "y": 225.99, "z": 12.61}, {"x": 1489.67, "y": 226.3, "z": 12.58}, {"x": 1488.42, "y": 226.49, "z": 12.56}, {"x": 1487.31, "y": 226.59, "z": 12.54}, {"x": 1486.29, "y": 226.45, "z": 12.52}, {"x": 1485.17, "y": 226.14, "z": 12.53}, {"x": 1483.78, "y": 225.64, "z": 12.53}], "right_lane_mark_type": "NONE", "successors": [42807335], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806682": {"id": 42806682, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1510.24, "y": 228.05, "z": 12.62}, {"x": 1508.54, "y": 227.37, "z": 12.62}, {"x": 1507.48, "y": 226.57, "z": 12.62}, {"x": 1506.54, "y": 225.81, "z": 12.62}, {"x": 1505.35, "y": 224.76, "z": 12.61}, {"x": 1504.3, "y": 223.67, "z": 12.61}, {"x": 1503.37, "y": 222.62, "z": 12.6}, {"x": 1502.28, "y": 221.08, "z": 12.6}, {"x": 1501.54, "y": 219.68, "z": 12.59}, {"x": 1501.08, "y": 218.33, "z": 12.59}, {"x": 1500.85, "y": 217.05, "z": 12.59}, {"x": 1500.9, "y": 215.72, "z": 12.59}, {"x": 1501.15, "y": 214.35, "z": 12.62}, {"x": 1501.44, "y": 213.31, "z": 12.64}, {"x": 1502.42, "y": 210.24, "z": 12.7}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1509.06, "y": 231.1, "z": 12.57}, {"x": 1507.7, "y": 230.52, "z": 12.57}, {"x": 1506.44, "y": 229.91, "z": 12.6}, {"x": 1505.32, "y": 229.13, "z": 12.63}, {"x": 1503.98, "y": 228.03, "z": 12.64}, {"x": 1502.86, "y": 226.82, "z": 12.65}, {"x": 1501.57, "y": 225.62, "z": 12.65}, {"x": 1500.25, "y": 224.09, "z": 12.64}, {"x": 1499.05, "y": 222.63, "z": 12.63}, {"x": 1497.81, "y": 220.98, "z": 12.63}, {"x": 1496.71, "y": 219.06, "z": 12.63}, {"x": 1496.05, "y": 217.6, "z": 12.64}, {"x": 1495.66, "y": 216.24, "z": 12.64}, {"x": 1495.51, "y": 214.95, "z": 12.63}, {"x": 1495.44, "y": 213.25, "z": 12.66}, {"x": 1495.54, "y": 211.86, "z": 12.68}, {"x": 1495.84, "y": 210.67, "z": 12.71}, {"x": 1496.78, "y": 208.12, "z": 12.74}], "right_lane_mark_type": "NONE", "successors": [42811329], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806684": {"id": 42806684, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1507.87, "y": 234.12, "z": 12.39}, {"x": 1505.72, "y": 233.33, "z": 12.37}, {"x": 1504.06, "y": 232.9, "z": 12.38}, {"x": 1502.53, "y": 232.64, "z": 12.4}, {"x": 1501.63, "y": 232.64, "z": 12.4}, {"x": 1500.68, "y": 232.79, "z": 12.38}, {"x": 1499.93, "y": 233.04, "z": 12.36}, {"x": 1499.23, "y": 233.44, "z": 12.32}, {"x": 1498.53, "y": 234.04, "z": 12.29}, {"x": 1497.68, "y": 234.99, "z": 12.26}, {"x": 1496.98, "y": 236.04, "z": 12.25}, {"x": 1496.33, "y": 237.34, "z": 12.22}, {"x": 1495.68, "y": 239.04, "z": 12.19}, {"x": 1495.48, "y": 239.66, "z": 12.18}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1506.47, "y": 237.27, "z": 12.24}, {"x": 1504.59, "y": 236.65, "z": 12.23}, {"x": 1503.28, "y": 236.29, "z": 12.21}, {"x": 1502.11, "y": 236.28, "z": 12.19}, {"x": 1501.25, "y": 236.58, "z": 12.17}, {"x": 1500.57, "y": 237.15, "z": 12.16}, {"x": 1500.06, "y": 237.65, "z": 12.18}, {"x": 1499.55, "y": 238.26, "z": 12.18}, {"x": 1498.95, "y": 239.03, "z": 12.18}, {"x": 1498.46, "y": 239.86, "z": 12.18}], "right_lane_mark_type": "NONE", "successors": [42811961], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806871": {"id": 42806871, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1479.55, "y": 321.04, "z": 11.23}, {"x": 1476.51, "y": 320.14, "z": 11.26}, {"x": 1475.61, "y": 319.99, "z": 11.26}, {"x": 1474.77, "y": 319.94, "z": 11.26}, {"x": 1473.97, "y": 319.94, "z": 11.26}, {"x": 1473.22, "y": 319.99, "z": 11.27}, {"x": 1471.42, "y": 320.29, "z": 11.26}, {"x": 1469.83, "y": 320.69, "z": 11.26}, {"x": 1469.03, "y": 320.99, "z": 11.25}, {"x": 1468.28, "y": 321.34, "z": 11.24}, {"x": 1467.68, "y": 321.64, "z": 11.23}, {"x": 1467.08, "y": 322.04, "z": 11.22}, {"x": 1466.28, "y": 322.64, "z": 11.2}, {"x": 1465.68, "y": 323.29, "z": 11.18}, {"x": 1465.08, "y": 324.24, "z": 11.15}, {"x": 1464.68, "y": 324.94, "z": 11.12}, {"x": 1462.26, "y": 330.92, "z": 10.96}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1478.34, "y": 324.43, "z": 11.23}, {"x": 1477.5, "y": 324.15, "z": 11.22}, {"x": 1476.99, "y": 324.1, "z": 11.23}, {"x": 1476.31, "y": 324.13, "z": 11.23}, {"x": 1475.52, "y": 324.31, "z": 11.24}, {"x": 1474.78, "y": 324.57, "z": 11.25}, {"x": 1474.25, "y": 324.77, "z": 11.25}, {"x": 1473.62, "y": 324.97, "z": 11.23}, {"x": 1472.4, "y": 325.42, "z": 11.18}, {"x": 1471.66, "y": 325.81, "z": 11.17}, {"x": 1469.87, "y": 326.59, "z": 11.09}, {"x": 1468.94, "y": 327.12, "z": 11.07}, {"x": 1467.87, "y": 328.01, "z": 11.06}, {"x": 1467.38, "y": 328.5, "z": 11.05}, {"x": 1466.89, "y": 329.14, "z": 11.03}, {"x": 1466.29, "y": 329.87, "z": 11.03}, {"x": 1465.68, "y": 330.87, "z": 11.03}, {"x": 1465.15, "y": 331.9, "z": 10.99}], "right_lane_mark_type": "NONE", "successors": [42817683], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806873": {"id": 42806873, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1480.7, "y": 318.11, "z": 11.27}, {"x": 1478.01, "y": 317.04, "z": 11.28}, {"x": 1473.92, "y": 315.59, "z": 11.3}, {"x": 1471.73, "y": 314.74, "z": 11.33}, {"x": 1471.08, "y": 314.39, "z": 11.34}, {"x": 1470.38, "y": 313.94, "z": 11.35}, {"x": 1469.73, "y": 313.39, "z": 11.35}, {"x": 1469.18, "y": 312.74, "z": 11.36}, {"x": 1468.62, "y": 311.84, "z": 11.37}, {"x": 1468.22, "y": 310.8, "z": 11.37}, {"x": 1467.98, "y": 309.65, "z": 11.37}, {"x": 1467.98, "y": 308.65, "z": 11.37}, {"x": 1467.98, "y": 307.75, "z": 11.38}, {"x": 1468.12, "y": 306.1, "z": 11.4}, {"x": 1468.25, "y": 305.21, "z": 11.41}, {"x": 1468.52, "y": 304.26, "z": 11.42}, {"x": 1469.41, "y": 302.25, "z": 11.44}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1479.55, "y": 321.04, "z": 11.23}, {"x": 1467.59, "y": 316.71, "z": 11.34}, {"x": 1466.45, "y": 316.25, "z": 11.34}, {"x": 1465.51, "y": 315.62, "z": 11.35}, {"x": 1464.65, "y": 314.84, "z": 11.37}, {"x": 1463.87, "y": 313.9, "z": 11.39}, {"x": 1463.16, "y": 312.8, "z": 11.4}, {"x": 1462.53, "y": 311.39, "z": 11.43}, {"x": 1462.06, "y": 309.9, "z": 11.43}, {"x": 1461.74, "y": 308.25, "z": 11.43}, {"x": 1461.66, "y": 306.45, "z": 11.41}, {"x": 1461.82, "y": 304.33, "z": 11.41}, {"x": 1462.92, "y": 300.07, "z": 11.41}], "right_lane_mark_type": "NONE", "successors": [42809705], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806877": {"id": 42806877, "is_intersection": true, "lane_type": "BUS", "left_lane_boundary": [{"x": 1398.98, "y": 188.67, "z": 13.09}, {"x": 1397.1, "y": 187.89, "z": 13.08}, {"x": 1393.18, "y": 186.37, "z": 13.09}, {"x": 1391.35, "y": 185.58, "z": 13.11}, {"x": 1390.13, "y": 184.8, "z": 13.12}, {"x": 1389.32, "y": 184.05, "z": 13.13}, {"x": 1388.57, "y": 183.17, "z": 13.14}, {"x": 1387.99, "y": 182.16, "z": 13.15}, {"x": 1386.87, "y": 179.77, "z": 13.16}, {"x": 1382.75, "y": 169.07, "z": 13.47}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1397.99, "y": 191.56, "z": 13.03}, {"x": 1390.32, "y": 188.7, "z": 13.07}, {"x": 1388.98, "y": 188.23, "z": 13.07}, {"x": 1387.55, "y": 187.53, "z": 13.08}, {"x": 1386.09, "y": 186.59, "z": 13.1}, {"x": 1385.11, "y": 185.57, "z": 13.13}, {"x": 1384.36, "y": 184.54, "z": 13.15}, {"x": 1383.44, "y": 182.92, "z": 13.16}, {"x": 1382.65, "y": 181.14, "z": 13.16}, {"x": 1381.89, "y": 179.11, "z": 13.16}, {"x": 1380.45, "y": 174.8, "z": 13.17}, {"x": 1377.14, "y": 169.57, "z": 13.35}], "right_lane_mark_type": "NONE", "successors": [42808601], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806889": {"id": 42806889, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1430.9, "y": 303.85, "z": 11.72}, {"x": 1425.12, "y": 301.8, "z": 11.74}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1429.9, "y": 306.83, "z": 11.67}, {"x": 1424.09, "y": 304.75, "z": 11.73}], "right_lane_mark_type": "NONE", "successors": [42808178], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42809667}, "42806903": {"id": 42806903, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1378.7, "y": 160.95, "z": 13.72}, {"x": 1382.75, "y": 169.07, "z": 13.47}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1382.0, "y": 159.55, "z": 13.74}, {"x": 1386.01, "y": 167.54, "z": 13.52}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42806535], "predecessors": [], "right_neighbor_id": 42811887, "left_neighbor_id": 42808601}, "42806907": {"id": 42806907, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1463.29, "y": 207.87, "z": 12.81}, {"x": 1479.88, "y": 213.79, "z": 12.72}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1464.47, "y": 204.69, "z": 12.8}, {"x": 1480.99, "y": 210.73, "z": 12.69}], "right_lane_mark_type": "NONE", "successors": [42808620], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42811487}, "42806926": {"id": 42806926, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1482.45, "y": 265.91, "z": 11.89}, {"x": 1481.89, "y": 267.01, "z": 11.87}, {"x": 1481.46, "y": 267.57, "z": 11.85}, {"x": 1480.82, "y": 267.88, "z": 11.85}, {"x": 1479.72, "y": 267.77, "z": 11.84}, {"x": 1473.3, "y": 265.58, "z": 11.89}, {"x": 1473.27, "y": 265.58, "z": 11.89}, {"x": 1471.19, "y": 264.87, "z": 11.98}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1485.5, "y": 266.88, "z": 11.81}, {"x": 1484.86, "y": 268.88, "z": 11.78}, {"x": 1484.01, "y": 271.13, "z": 11.75}, {"x": 1483.36, "y": 272.08, "z": 11.74}, {"x": 1482.47, "y": 272.79, "z": 11.75}, {"x": 1481.58, "y": 273.2, "z": 11.76}, {"x": 1480.69, "y": 273.32, "z": 11.77}, {"x": 1479.63, "y": 273.09, "z": 11.79}, {"x": 1469.26, "y": 270.09, "z": 11.95}], "right_lane_mark_type": "NONE", "successors": [42806482], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42806933": {"id": 42806933, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1502.42, "y": 210.24, "z": 12.7}, {"x": 1492.05, "y": 238.97, "z": 12.28}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1508.47, "y": 212.44, "z": 12.71}, {"x": 1495.48, "y": 239.66, "z": 12.18}], "right_lane_mark_type": "NONE", "successors": [42810834], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42807745}, "42807330": {"id": 42807330, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1469.41, "y": 302.25, "z": 11.44}, {"x": 1467.92, "y": 305.85, "z": 11.4}, {"x": 1467.23, "y": 307.2, "z": 11.39}, {"x": 1466.68, "y": 308.0, "z": 11.38}, {"x": 1465.93, "y": 308.75, "z": 11.39}, {"x": 1464.98, "y": 309.35, "z": 11.4}, {"x": 1464.13, "y": 309.75, "z": 11.41}, {"x": 1463.04, "y": 310.05, "z": 11.43}, {"x": 1461.39, "y": 310.2, "z": 11.44}, {"x": 1460.19, "y": 310.2, "z": 11.45}, {"x": 1458.99, "y": 310.05, "z": 11.46}, {"x": 1457.54, "y": 309.75, "z": 11.47}, {"x": 1456.04, "y": 309.35, "z": 11.49}, {"x": 1453.89, "y": 308.6, "z": 11.5}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1472.35, "y": 303.23, "z": 11.4}, {"x": 1471.69, "y": 305.02, "z": 11.37}, {"x": 1470.56, "y": 307.74, "z": 11.36}, {"x": 1469.8, "y": 309.69, "z": 11.35}, {"x": 1468.91, "y": 311.52, "z": 11.36}, {"x": 1468.37, "y": 312.35, "z": 11.37}, {"x": 1467.49, "y": 313.24, "z": 11.38}, {"x": 1466.66, "y": 313.65, "z": 11.38}, {"x": 1465.59, "y": 314.05, "z": 11.38}, {"x": 1464.18, "y": 314.29, "z": 11.38}, {"x": 1462.81, "y": 314.3, "z": 11.38}, {"x": 1461.39, "y": 314.12, "z": 11.4}, {"x": 1459.79, "y": 313.82, "z": 11.4}, {"x": 1458.38, "y": 313.5, "z": 11.41}, {"x": 1457.24, "y": 313.09, "z": 11.42}, {"x": 1455.75, "y": 312.64, "z": 11.42}, {"x": 1452.85, "y": 311.64, "z": 11.44}], "right_lane_mark_type": "NONE", "successors": [42809364], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42807335": {"id": 42807335, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1484.88, "y": 222.63, "z": 12.67}, {"x": 1477.63, "y": 220.04, "z": 12.71}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1483.78, "y": 225.64, "z": 12.53}, {"x": 1476.59, "y": 223.05, "z": 12.57}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42810769], "predecessors": [42806420, 42806677], "right_neighbor_id": null, "left_neighbor_id": 42811286}, "42807338": {"id": 42807338, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1600.38, "y": 246.57, "z": 12.62}, {"x": 1601.71, "y": 245.81, "z": 12.64}, {"x": 1603.63, "y": 244.18, "z": 12.7}, {"x": 1604.79, "y": 242.74, "z": 12.76}, {"x": 1605.44, "y": 241.47, "z": 12.8}, {"x": 1605.9, "y": 240.01, "z": 12.85}, {"x": 1606.04, "y": 238.45, "z": 12.89}, {"x": 1605.96, "y": 236.65, "z": 12.94}, {"x": 1605.53, "y": 234.92, "z": 12.98}, {"x": 1604.83, "y": 233.17, "z": 13.03}, {"x": 1601.14, "y": 226.04, "z": 13.24}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1598.89, "y": 243.49, "z": 12.65}, {"x": 1599.62, "y": 242.72, "z": 12.67}, {"x": 1600.38, "y": 241.61, "z": 12.71}, {"x": 1600.84, "y": 239.99, "z": 12.76}, {"x": 1601.07, "y": 238.49, "z": 12.79}, {"x": 1601.23, "y": 236.89, "z": 12.83}, {"x": 1601.26, "y": 235.11, "z": 12.9}, {"x": 1600.77, "y": 233.48, "z": 12.97}, {"x": 1599.95, "y": 231.63, "z": 13.04}, {"x": 1599.76, "y": 231.0, "z": 13.04}, {"x": 1598.24, "y": 227.94, "z": 13.2}, {"x": 1598.06, "y": 227.53, "z": 13.19}], "right_lane_mark_type": "NONE", "successors": [42819444], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42807339": {"id": 42807339, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1603.38, "y": 253.1, "z": 12.61}, {"x": 1604.64, "y": 252.69, "z": 12.59}, {"x": 1605.98, "y": 252.28, "z": 12.57}, {"x": 1607.52, "y": 251.95, "z": 12.56}, {"x": 1608.87, "y": 251.82, "z": 12.55}, {"x": 1610.85, "y": 251.91, "z": 12.54}, {"x": 1613.0, "y": 252.26, "z": 12.51}, {"x": 1615.82, "y": 252.7, "z": 12.48}, {"x": 1618.33, "y": 253.45, "z": 12.46}, {"x": 1620.66, "y": 254.58, "z": 12.42}, {"x": 1622.83, "y": 256.27, "z": 12.36}, {"x": 1624.68, "y": 257.75, "z": 12.31}, {"x": 1626.49, "y": 259.46, "z": 12.24}, {"x": 1629.07, "y": 262.2, "z": 12.12}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1601.93, "y": 249.85, "z": 12.6}, {"x": 1603.87, "y": 249.16, "z": 12.62}, {"x": 1605.13, "y": 248.83, "z": 12.63}, {"x": 1607.23, "y": 248.46, "z": 12.64}, {"x": 1609.66, "y": 248.31, "z": 12.66}, {"x": 1612.27, "y": 248.42, "z": 12.65}, {"x": 1615.05, "y": 248.87, "z": 12.62}, {"x": 1617.83, "y": 249.66, "z": 12.58}, {"x": 1620.51, "y": 250.93, "z": 12.52}, {"x": 1622.77, "y": 252.35, "z": 12.46}, {"x": 1624.96, "y": 253.97, "z": 12.4}, {"x": 1631.2, "y": 260.05, "z": 12.11}], "right_lane_mark_type": "NONE", "successors": [42812503], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42807471": {"id": 42807471, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1492.05, "y": 238.97, "z": 12.28}, {"x": 1492.76, "y": 236.7, "z": 12.31}, {"x": 1493.03, "y": 235.93, "z": 12.32}, {"x": 1493.41, "y": 235.22, "z": 12.34}, {"x": 1493.89, "y": 234.41, "z": 12.35}, {"x": 1494.62, "y": 233.64, "z": 12.36}, {"x": 1495.44, "y": 232.99, "z": 12.36}, {"x": 1496.51, "y": 232.21, "z": 12.38}, {"x": 1497.61, "y": 231.37, "z": 12.42}, {"x": 1499.04, "y": 230.49, "z": 12.48}, {"x": 1500.42, "y": 229.74, "z": 12.54}, {"x": 1501.73, "y": 229.06, "z": 12.6}, {"x": 1503.25, "y": 228.41, "z": 12.63}, {"x": 1504.51, "y": 228.0, "z": 12.64}, {"x": 1505.69, "y": 227.82, "z": 12.65}, {"x": 1506.95, "y": 227.6, "z": 12.64}, {"x": 1508.22, "y": 227.48, "z": 12.62}, {"x": 1509.31, "y": 227.73, "z": 12.62}, {"x": 1510.24, "y": 228.05, "z": 12.62}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1488.78, "y": 237.89, "z": 12.27}, {"x": 1489.53, "y": 235.58, "z": 12.31}, {"x": 1489.89, "y": 234.84, "z": 12.33}, {"x": 1490.29, "y": 233.98, "z": 12.36}, {"x": 1490.88, "y": 233.11, "z": 12.39}, {"x": 1491.39, "y": 232.3, "z": 12.4}, {"x": 1492.15, "y": 231.45, "z": 12.42}, {"x": 1492.81, "y": 230.66, "z": 12.44}, {"x": 1493.65, "y": 229.83, "z": 12.48}, {"x": 1494.73, "y": 228.99, "z": 12.52}, {"x": 1495.81, "y": 228.29, "z": 12.56}, {"x": 1497.27, "y": 227.46, "z": 12.61}, {"x": 1498.5, "y": 226.8, "z": 12.66}, {"x": 1499.83, "y": 226.14, "z": 12.66}, {"x": 1501.31, "y": 225.43, "z": 12.65}, {"x": 1502.71, "y": 224.85, "z": 12.63}, {"x": 1503.8, "y": 224.51, "z": 12.62}, {"x": 1504.74, "y": 224.4, "z": 12.61}, {"x": 1505.97, "y": 224.33, "z": 12.59}, {"x": 1507.22, "y": 224.31, "z": 12.59}, {"x": 1508.13, "y": 224.44, "z": 12.58}, {"x": 1509.02, "y": 224.54, "z": 12.58}, {"x": 1509.83, "y": 224.73, "z": 12.57}, {"x": 1511.26, "y": 225.25, "z": 12.58}], "right_lane_mark_type": "NONE", "successors": [42811495], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42807473": {"id": 42807473, "is_intersection": false, "lane_type": "BUS", "left_lane_boundary": [{"x": 1440.0, "y": 199.54, "z": 12.94}, {"x": 1444.15, "y": 200.94, "z": 12.9}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1440.0, "y": 195.91, "z": 12.95}, {"x": 1445.2, "y": 197.77, "z": 12.92}], "right_lane_mark_type": "NONE", "successors": [42810413], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42809731}, "42807547": {"id": 42807547, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1456.07, "y": 328.82, "z": 10.93}, {"x": 1459.24, "y": 319.29, "z": 11.25}, {"x": 1459.54, "y": 318.14, "z": 11.29}, {"x": 1459.64, "y": 317.24, "z": 11.31}, {"x": 1459.49, "y": 316.14, "z": 11.34}, {"x": 1458.94, "y": 315.09, "z": 11.37}, {"x": 1458.39, "y": 314.44, "z": 11.39}, {"x": 1457.79, "y": 313.89, "z": 11.4}, {"x": 1456.99, "y": 313.34, "z": 11.42}, {"x": 1455.9, "y": 312.79, "z": 11.42}, {"x": 1452.85, "y": 311.64, "z": 11.44}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1452.8, "y": 327.68, "z": 10.92}, {"x": 1452.8, "y": 327.66, "z": 10.92}, {"x": 1455.3, "y": 320.67, "z": 11.17}, {"x": 1455.57, "y": 319.65, "z": 11.21}, {"x": 1455.63, "y": 318.74, "z": 11.23}, {"x": 1455.52, "y": 317.69, "z": 11.25}, {"x": 1455.15, "y": 316.93, "z": 11.27}, {"x": 1454.47, "y": 316.02, "z": 11.31}, {"x": 1453.68, "y": 315.39, "z": 11.34}, {"x": 1452.97, "y": 315.05, "z": 11.35}, {"x": 1452.45, "y": 314.84, "z": 11.39}, {"x": 1451.77, "y": 314.62, "z": 11.41}], "right_lane_mark_type": "NONE", "successors": [42811474], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42807573": {"id": 42807573, "is_intersection": true, "lane_type": "BUS", "left_lane_boundary": [{"x": 1368.91, "y": 174.38, "z": 13.18}, {"x": 1374.42, "y": 176.27, "z": 13.17}, {"x": 1376.62, "y": 176.79, "z": 13.17}, {"x": 1377.81, "y": 176.88, "z": 13.16}, {"x": 1379.53, "y": 176.63, "z": 13.17}, {"x": 1380.76, "y": 176.2, "z": 13.17}, {"x": 1381.98, "y": 175.35, "z": 13.17}, {"x": 1383.02, "y": 173.77, "z": 13.19}, {"x": 1383.27, "y": 172.55, "z": 13.26}, {"x": 1383.3, "y": 171.39, "z": 13.35}, {"x": 1382.75, "y": 169.07, "z": 13.47}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1370.06, "y": 170.87, "z": 13.2}, {"x": 1371.55, "y": 171.4, "z": 13.26}, {"x": 1372.77, "y": 171.77, "z": 13.21}, {"x": 1373.66, "y": 171.96, "z": 13.18}, {"x": 1374.17, "y": 171.99, "z": 13.18}, {"x": 1374.59, "y": 171.89, "z": 13.18}, {"x": 1374.75, "y": 171.85, "z": 13.19}, {"x": 1375.29, "y": 171.63, "z": 13.2}, {"x": 1375.89, "y": 171.3, "z": 13.23}, {"x": 1376.17, "y": 171.01, "z": 13.25}, {"x": 1376.22, "y": 170.96, "z": 13.25}, {"x": 1376.6, "y": 170.49, "z": 13.29}, {"x": 1376.94, "y": 169.98, "z": 13.31}, {"x": 1377.14, "y": 169.57, "z": 13.35}], "right_lane_mark_type": "NONE", "successors": [42808601], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42807643": {"id": 42807643, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1577.64, "y": 255.23, "z": 12.55}, {"x": 1578.92, "y": 255.44, "z": 12.54}, {"x": 1581.88, "y": 255.77, "z": 12.55}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1578.81, "y": 251.78, "z": 12.53}, {"x": 1581.18, "y": 252.24, "z": 12.53}, {"x": 1582.28, "y": 252.36, "z": 12.54}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42915538], "predecessors": [], "right_neighbor_id": 42812483, "left_neighbor_id": null}, "42807644": {"id": 42807644, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1502.42, "y": 210.24, "z": 12.7}, {"x": 1501.93, "y": 212.16, "z": 12.66}, {"x": 1501.54, "y": 214.04, "z": 12.63}, {"x": 1501.38, "y": 215.29, "z": 12.61}, {"x": 1501.38, "y": 216.54, "z": 12.59}, {"x": 1501.67, "y": 218.02, "z": 12.58}, {"x": 1502.33, "y": 219.48, "z": 12.59}, {"x": 1503.35, "y": 220.78, "z": 12.59}, {"x": 1504.35, "y": 221.78, "z": 12.59}, {"x": 1505.74, "y": 222.71, "z": 12.58}, {"x": 1507.21, "y": 223.49, "z": 12.58}, {"x": 1509.0, "y": 224.34, "z": 12.57}, {"x": 1511.26, "y": 225.25, "z": 12.58}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1508.47, "y": 212.44, "z": 12.71}, {"x": 1507.81, "y": 214.18, "z": 12.65}, {"x": 1507.54, "y": 215.43, "z": 12.65}, {"x": 1507.48, "y": 216.51, "z": 12.62}, {"x": 1507.63, "y": 217.59, "z": 12.59}, {"x": 1507.9, "y": 218.5, "z": 12.57}, {"x": 1508.43, "y": 219.32, "z": 12.57}, {"x": 1508.99, "y": 220.0, "z": 12.57}, {"x": 1509.81, "y": 220.65, "z": 12.59}, {"x": 1510.55, "y": 221.05, "z": 12.57}, {"x": 1512.4, "y": 221.74, "z": 12.6}], "right_lane_mark_type": "NONE", "successors": [42811280], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42807745": {"id": 42807745, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1492.05, "y": 238.97, "z": 12.28}, {"x": 1502.42, "y": 210.24, "z": 12.7}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1488.78, "y": 237.89, "z": 12.27}, {"x": 1490.76, "y": 232.36, "z": 12.4}, {"x": 1491.28, "y": 229.97, "z": 12.45}, {"x": 1494.12, "y": 214.68, "z": 12.64}, {"x": 1496.78, "y": 208.12, "z": 12.74}], "right_lane_mark_type": "NONE", "successors": [42811329], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42806933}, "42807779": {"id": 42807779, "is_intersection": true, "lane_type": "BUS", "left_lane_boundary": [{"x": 1629.14, "y": 234.39, "z": 13.12}, {"x": 1628.25, "y": 234.92, "z": 13.03}, {"x": 1625.06, "y": 236.75, "z": 12.91}, {"x": 1622.74, "y": 237.53, "z": 12.89}, {"x": 1620.48, "y": 237.99, "z": 12.89}, {"x": 1617.83, "y": 238.09, "z": 12.91}, {"x": 1615.73, "y": 237.93, "z": 12.93}, {"x": 1612.96, "y": 237.34, "z": 12.96}, {"x": 1610.99, "y": 236.13, "z": 13.0}, {"x": 1610.3, "y": 235.55, "z": 13.02}, {"x": 1609.65, "y": 234.68, "z": 13.05}, {"x": 1609.09, "y": 233.53, "z": 13.08}, {"x": 1604.2, "y": 224.73, "z": 13.36}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1631.46, "y": 237.92, "z": 12.93}, {"x": 1629.34, "y": 238.93, "z": 12.84}, {"x": 1627.28, "y": 239.79, "z": 12.8}, {"x": 1625.34, "y": 240.56, "z": 12.77}, {"x": 1623.2, "y": 241.31, "z": 12.77}, {"x": 1621.0, "y": 241.69, "z": 12.78}, {"x": 1619.58, "y": 241.78, "z": 12.79}, {"x": 1617.63, "y": 241.78, "z": 12.8}, {"x": 1614.94, "y": 241.55, "z": 12.83}, {"x": 1611.88, "y": 240.51, "z": 12.87}, {"x": 1610.13, "y": 239.67, "z": 12.9}, {"x": 1608.69, "y": 238.59, "z": 12.92}, {"x": 1607.46, "y": 237.11, "z": 12.96}, {"x": 1606.49, "y": 235.26, "z": 12.99}, {"x": 1601.14, "y": 226.04, "z": 13.24}], "right_lane_mark_type": "NONE", "successors": [42819224], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42807784": {"id": 42807784, "is_intersection": true, "lane_type": "BUS", "left_lane_boundary": [{"x": 1600.38, "y": 246.57, "z": 12.62}, {"x": 1600.81, "y": 246.38, "z": 12.62}, {"x": 1601.79, "y": 245.84, "z": 12.64}, {"x": 1607.17, "y": 241.21, "z": 12.83}, {"x": 1623.78, "y": 228.03, "z": 13.25}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1598.89, "y": 243.49, "z": 12.65}, {"x": 1620.95, "y": 223.02, "z": 13.3}], "right_lane_mark_type": "NONE", "successors": [42806912], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42819897}, "42808012": {"id": 42808012, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1480.7, "y": 318.11, "z": 11.27}, {"x": 1453.89, "y": 308.6, "z": 11.5}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1479.55, "y": 321.04, "z": 11.23}, {"x": 1452.85, "y": 311.64, "z": 11.44}], "right_lane_mark_type": "NONE", "successors": [42809364], "predecessors": [], "right_neighbor_id": 42811457, "left_neighbor_id": null}, "42808033": {"id": 42808033, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1472.35, "y": 303.23, "z": 11.4}, {"x": 1462.26, "y": 330.92, "z": 10.96}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1475.18, "y": 304.11, "z": 11.35}, {"x": 1474.47, "y": 306.08, "z": 11.39}, {"x": 1474.21, "y": 306.74, "z": 11.36}, {"x": 1465.15, "y": 331.9, "z": 10.99}], "right_lane_mark_type": "NONE", "successors": [42817683], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42808643}, "42808440": {"id": 42808440, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1350.0, "y": 269.27, "z": 12.13}, {"x": 1422.05, "y": 294.85, "z": 11.76}], "left_lane_mark_type": "DASHED_YELLOW", "right_lane_boundary": [{"x": 1350.0, "y": 267.75, "z": 12.13}, {"x": 1422.38, "y": 293.33, "z": 11.8}, {"x": 1422.53, "y": 293.39, "z": 11.8}], "right_lane_mark_type": "NONE", "successors": [42811247], "predecessors": [42806887], "right_neighbor_id": null, "left_neighbor_id": 42811879}, "42808559": {"id": 42808559, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1440.0, "y": 301.22, "z": 11.7}, {"x": 1426.94, "y": 296.59, "z": 11.73}], "left_lane_mark_type": "DASHED_YELLOW", "right_lane_boundary": [{"x": 1440.0, "y": 302.96, "z": 11.71}, {"x": 1428.29, "y": 298.73, "z": 11.76}, {"x": 1426.42, "y": 298.06, "z": 11.76}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42811311], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42811479}, "42808583": {"id": 42808583, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1442.94, "y": 204.48, "z": 12.92}, {"x": 1445.02, "y": 205.54, "z": 12.92}, {"x": 1455.39, "y": 211.78, "z": 12.83}, {"x": 1457.25, "y": 212.64, "z": 12.82}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1444.15, "y": 200.94, "z": 12.9}, {"x": 1445.76, "y": 201.57, "z": 12.89}, {"x": 1456.84, "y": 208.85, "z": 12.86}, {"x": 1458.33, "y": 209.64, "z": 12.86}], "right_lane_mark_type": "NONE", "successors": [42809311], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42809376}, "42808597": {"id": 42808597, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1367.83, "y": 177.56, "z": 13.22}, {"x": 1398.98, "y": 188.67, "z": 13.09}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1368.91, "y": 174.38, "z": 13.18}, {"x": 1400.28, "y": 185.29, "z": 13.07}], "right_lane_mark_type": "NONE", "successors": [42810791], "predecessors": [], "right_neighbor_id": 42809914, "left_neighbor_id": 42809419}, "42808601": {"id": 42808601, "is_intersection": false, "lane_type": "BUS", "left_lane_boundary": [{"x": 1382.75, "y": 169.07, "z": 13.47}, {"x": 1378.7, "y": 160.95, "z": 13.72}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1377.14, "y": 169.57, "z": 13.35}, {"x": 1377.4, "y": 168.36, "z": 13.42}, {"x": 1377.43, "y": 167.74, "z": 13.45}, {"x": 1377.4, "y": 166.99, "z": 13.48}, {"x": 1376.98, "y": 166.21, "z": 13.48}, {"x": 1375.01, "y": 162.53, "z": 13.61}], "right_lane_mark_type": "NONE", "successors": [42818876], "predecessors": [42807573, 42806877], "right_neighbor_id": null, "left_neighbor_id": 42806903}, "42808620": {"id": 42808620, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1479.88, "y": 213.79, "z": 12.72}, {"x": 1487.16, "y": 216.4, "z": 12.69}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1480.99, "y": 210.73, "z": 12.69}, {"x": 1488.27, "y": 213.4, "z": 12.68}], "right_lane_mark_type": "NONE", "successors": [42810795, 42806422], "predecessors": [42806907], "right_neighbor_id": null, "left_neighbor_id": 42811322}, "42808641": {"id": 42808641, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1482.11, "y": 276.35, "z": 11.67}, {"x": 1472.35, "y": 303.23, "z": 11.4}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1484.98, "y": 277.16, "z": 11.62}, {"x": 1475.18, "y": 304.11, "z": 11.35}], "right_lane_mark_type": "NONE", "successors": [42808033, 42817814], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42808644}, "42808642": {"id": 42808642, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1485.5, "y": 266.88, "z": 11.81}, {"x": 1482.11, "y": 276.35, "z": 11.67}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1488.39, "y": 267.72, "z": 11.76}, {"x": 1484.98, "y": 277.16, "z": 11.62}], "right_lane_mark_type": "NONE", "successors": [42808641], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42810767}, "42808643": {"id": 42808643, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1469.41, "y": 302.25, "z": 11.44}, {"x": 1459.23, "y": 329.93, "z": 10.98}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1472.35, "y": 303.23, "z": 11.4}, {"x": 1462.26, "y": 330.92, "z": 10.96}], "right_lane_mark_type": "NONE", "successors": [42817689], "predecessors": [], "right_neighbor_id": 42808033, "left_neighbor_id": 42806338}, "42808644": {"id": 42808644, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1478.91, "y": 275.45, "z": 11.75}, {"x": 1469.41, "y": 302.25, "z": 11.44}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1482.11, "y": 276.35, "z": 11.67}, {"x": 1472.35, "y": 303.23, "z": 11.4}], "right_lane_mark_type": "NONE", "successors": [42808643, 42807330, 42817783], "predecessors": [], "right_neighbor_id": 42808641, "left_neighbor_id": 42809705}, "42808745": {"id": 42808745, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1495.36, "y": 240.0, "z": 12.17}, {"x": 1492.93, "y": 246.98, "z": 12.07}, {"x": 1485.5, "y": 266.88, "z": 11.81}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1498.41, "y": 240.0, "z": 12.16}, {"x": 1489.2, "y": 265.5, "z": 11.76}, {"x": 1488.39, "y": 267.72, "z": 11.76}], "right_lane_mark_type": "NONE", "successors": [42808642], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42811679}, "42808752": {"id": 42808752, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1481.38, "y": 316.09, "z": 11.26}, {"x": 1513.3, "y": 327.25, "z": 11.41}], "left_lane_mark_type": "DASHED_YELLOW", "right_lane_boundary": [{"x": 1481.97, "y": 314.63, "z": 11.28}, {"x": 1489.76, "y": 317.43, "z": 11.32}, {"x": 1496.74, "y": 319.84, "z": 11.33}, {"x": 1513.89, "y": 325.98, "z": 11.45}], "right_lane_mark_type": "NONE", "successors": [42810629], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42808753}, "42808753": {"id": 42808753, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1513.3, "y": 327.25, "z": 11.41}, {"x": 1481.38, "y": 316.09, "z": 11.26}], "left_lane_mark_type": "DASHED_YELLOW", "right_lane_boundary": [{"x": 1512.7, "y": 328.5, "z": 11.42}, {"x": 1480.93, "y": 317.3, "z": 11.27}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42810822, 42817778], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42808752}, "42808948": {"id": 42808948, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1440.0, "y": 303.62, "z": 11.71}, {"x": 1437.58, "y": 302.7, "z": 11.72}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1440.0, "y": 307.04, "z": 11.65}, {"x": 1436.5, "y": 305.78, "z": 11.69}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42811456], "predecessors": [], "right_neighbor_id": 42810978, "left_neighbor_id": null}, "42808955": {"id": 42808955, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1421.42, "y": 296.98, "z": 11.79}, {"x": 1403.52, "y": 290.64, "z": 11.88}, {"x": 1378.04, "y": 281.63, "z": 11.99}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1420.34, "y": 300.11, "z": 11.74}, {"x": 1408.07, "y": 295.75, "z": 11.8}, {"x": 1396.93, "y": 291.76, "z": 11.84}, {"x": 1377.0, "y": 284.73, "z": 11.96}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42809671], "predecessors": [], "right_neighbor_id": 42809645, "left_neighbor_id": null}, "42808987": {"id": 42808987, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1454.58, "y": 306.44, "z": 11.49}, {"x": 1440.0, "y": 301.22, "z": 11.7}], "left_lane_mark_type": "DASHED_YELLOW", "right_lane_boundary": [{"x": 1453.98, "y": 307.86, "z": 11.51}, {"x": 1440.0, "y": 302.96, "z": 11.71}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42808559], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42810823}, "42809305": {"id": 42809305, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1442.94, "y": 204.48, "z": 12.92}, {"x": 1458.33, "y": 209.64, "z": 12.86}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1444.15, "y": 200.94, "z": 12.9}, {"x": 1459.46, "y": 206.46, "z": 12.82}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42809307], "predecessors": [], "right_neighbor_id": 42810413, "left_neighbor_id": null}, "42809307": {"id": 42809307, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1458.33, "y": 209.64, "z": 12.86}, {"x": 1462.12, "y": 210.95, "z": 12.84}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1459.46, "y": 206.46, "z": 12.82}, {"x": 1463.29, "y": 207.87, "z": 12.81}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42811487], "predecessors": [42809305], "right_neighbor_id": 42809309, "left_neighbor_id": 42809311}, "42809309": {"id": 42809309, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1459.46, "y": 206.46, "z": 12.82}, {"x": 1463.29, "y": 207.87, "z": 12.81}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1460.63, "y": 203.29, "z": 12.83}, {"x": 1464.47, "y": 204.69, "z": 12.8}], "right_lane_mark_type": "NONE", "successors": [42806907], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42809307}, "42809311": {"id": 42809311, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1457.25, "y": 212.64, "z": 12.82}, {"x": 1460.96, "y": 213.97, "z": 12.79}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1458.33, "y": 209.64, "z": 12.86}, {"x": 1462.12, "y": 210.95, "z": 12.84}], "right_lane_mark_type": "NONE", "successors": [42811445], "predecessors": [42808583], "right_neighbor_id": 42809307, "left_neighbor_id": 42809733}, "42809321": {"id": 42809321, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1525.14, "y": 230.26, "z": 12.58}, {"x": 1541.07, "y": 235.59, "z": 12.54}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1526.43, "y": 226.7, "z": 12.55}, {"x": 1542.17, "y": 232.27, "z": 12.51}], "right_lane_mark_type": "NONE", "successors": [42809329], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42811281}, "42809329": {"id": 42809329, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1541.07, "y": 235.59, "z": 12.54}, {"x": 1556.83, "y": 241.15, "z": 12.5}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1542.17, "y": 232.27, "z": 12.51}, {"x": 1558.05, "y": 237.87, "z": 12.47}], "right_lane_mark_type": "NONE", "successors": [42811491], "predecessors": [42809321], "right_neighbor_id": null, "left_neighbor_id": 42811505}, "42809364": {"id": 42809364, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1453.89, "y": 308.6, "z": 11.5}, {"x": 1440.0, "y": 303.62, "z": 11.71}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1452.85, "y": 311.64, "z": 11.44}, {"x": 1440.0, "y": 307.04, "z": 11.65}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42808948], "predecessors": [42807330, 42808012], "right_neighbor_id": 42811474, "left_neighbor_id": null}, "42809376": {"id": 42809376, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1457.25, "y": 212.64, "z": 12.82}, {"x": 1455.39, "y": 211.78, "z": 12.83}, {"x": 1445.02, "y": 205.54, "z": 12.92}, {"x": 1442.94, "y": 204.48, "z": 12.92}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1456.21, "y": 215.73, "z": 12.69}, {"x": 1453.9, "y": 214.49, "z": 12.71}, {"x": 1443.79, "y": 208.27, "z": 12.87}, {"x": 1442.01, "y": 207.33, "z": 12.9}], "right_lane_mark_type": "NONE", "successors": [42811963], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42808583}, "42809412": {"id": 42809412, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1478.91, "y": 275.45, "z": 11.75}, {"x": 1482.45, "y": 265.91, "z": 11.89}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1472.54, "y": 273.34, "z": 11.78}, {"x": 1479.05, "y": 264.73, "z": 11.87}], "right_lane_mark_type": "NONE", "successors": [42809413], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42810767}, "42809413": {"id": 42809413, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1482.45, "y": 265.91, "z": 11.89}, {"x": 1491.68, "y": 240.0, "z": 12.26}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1479.05, "y": 264.73, "z": 11.87}, {"x": 1487.99, "y": 240.0, "z": 12.24}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42810833], "predecessors": [42809412], "right_neighbor_id": null, "left_neighbor_id": 42811679}, "42809414": {"id": 42809414, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1625.21, "y": 265.4, "z": 12.03}, {"x": 1623.36, "y": 263.2, "z": 12.11}, {"x": 1621.47, "y": 260.9, "z": 12.2}, {"x": 1619.43, "y": 258.4, "z": 12.29}, {"x": 1617.72, "y": 255.96, "z": 12.37}, {"x": 1615.93, "y": 253.2, "z": 12.47}, {"x": 1613.9, "y": 249.82, "z": 12.59}, {"x": 1612.29, "y": 246.98, "z": 12.7}, {"x": 1610.72, "y": 244.15, "z": 12.78}, {"x": 1609.02, "y": 240.9, "z": 12.86}, {"x": 1607.13, "y": 237.41, "z": 12.94}, {"x": 1605.48, "y": 234.41, "z": 13.0}, {"x": 1601.14, "y": 226.04, "z": 13.24}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1622.41, "y": 267.96, "z": 12.02}, {"x": 1620.24, "y": 265.6, "z": 12.04}, {"x": 1616.84, "y": 261.5, "z": 12.19}, {"x": 1614.68, "y": 258.66, "z": 12.3}, {"x": 1612.82, "y": 255.95, "z": 12.38}, {"x": 1611.24, "y": 253.14, "z": 12.48}, {"x": 1609.64, "y": 250.13, "z": 12.6}, {"x": 1608.11, "y": 247.0, "z": 12.69}, {"x": 1606.42, "y": 243.77, "z": 12.76}, {"x": 1604.4, "y": 239.67, "z": 12.84}, {"x": 1602.31, "y": 235.66, "z": 12.89}, {"x": 1598.06, "y": 227.53, "z": 13.19}], "right_lane_mark_type": "NONE", "successors": [42819444], "predecessors": [42808587], "right_neighbor_id": null, "left_neighbor_id": 42809415}, "42809415": {"id": 42809415, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1627.89, "y": 262.92, "z": 12.13}, {"x": 1625.56, "y": 260.11, "z": 12.22}, {"x": 1622.62, "y": 256.24, "z": 12.36}, {"x": 1620.51, "y": 253.11, "z": 12.46}, {"x": 1618.22, "y": 249.75, "z": 12.57}, {"x": 1616.52, "y": 247.18, "z": 12.65}, {"x": 1614.74, "y": 244.34, "z": 12.76}, {"x": 1612.6, "y": 240.73, "z": 12.86}, {"x": 1609.22, "y": 234.19, "z": 13.06}, {"x": 1604.2, "y": 224.73, "z": 13.36}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1625.21, "y": 265.4, "z": 12.03}, {"x": 1623.36, "y": 263.2, "z": 12.11}, {"x": 1621.47, "y": 260.9, "z": 12.2}, {"x": 1619.43, "y": 258.4, "z": 12.29}, {"x": 1617.72, "y": 255.96, "z": 12.37}, {"x": 1615.93, "y": 253.2, "z": 12.47}, {"x": 1613.9, "y": 249.82, "z": 12.59}, {"x": 1612.29, "y": 246.98, "z": 12.7}, {"x": 1610.72, "y": 244.15, "z": 12.78}, {"x": 1609.02, "y": 240.9, "z": 12.86}, {"x": 1607.13, "y": 237.41, "z": 12.94}, {"x": 1605.48, "y": 234.41, "z": 13.0}, {"x": 1601.14, "y": 226.04, "z": 13.24}], "right_lane_mark_type": "NONE", "successors": [42819224], "predecessors": [], "right_neighbor_id": 42809414, "left_neighbor_id": null}, "42809419": {"id": 42809419, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1398.98, "y": 188.67, "z": 13.09}, {"x": 1367.83, "y": 177.56, "z": 13.22}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1397.99, "y": 191.56, "z": 13.03}, {"x": 1366.83, "y": 180.53, "z": 13.18}], "right_lane_mark_type": "NONE", "successors": [42808600], "predecessors": [], "right_neighbor_id": 42806291, "left_neighbor_id": 42808597}, "42809424": {"id": 42809424, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1486.03, "y": 219.35, "z": 12.73}, {"x": 1510.24, "y": 228.05, "z": 12.62}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1487.16, "y": 216.4, "z": 12.69}, {"x": 1511.26, "y": 225.25, "z": 12.58}], "right_lane_mark_type": "NONE", "successors": [42811495], "predecessors": [], "right_neighbor_id": 42810795, "left_neighbor_id": null}, "42809444": {"id": 42809444, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1426.19, "y": 298.68, "z": 11.77}, {"x": 1421.42, "y": 296.98, "z": 11.79}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1425.12, "y": 301.8, "z": 11.74}, {"x": 1420.34, "y": 300.11, "z": 11.74}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42808955], "predecessors": [], "right_neighbor_id": 42808178, "left_neighbor_id": null}, "42809667": {"id": 42809667, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1431.97, "y": 300.7, "z": 11.76}, {"x": 1426.19, "y": 298.68, "z": 11.77}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1430.9, "y": 303.85, "z": 11.72}, {"x": 1425.12, "y": 301.8, "z": 11.74}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42809444], "predecessors": [], "right_neighbor_id": 42806889, "left_neighbor_id": null}, "42809703": {"id": 42809703, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1512.32, "y": 329.13, "z": 11.42}, {"x": 1480.7, "y": 318.11, "z": 11.27}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1511.09, "y": 332.36, "z": 11.36}, {"x": 1479.55, "y": 321.04, "z": 11.23}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42808012, 42806873], "predecessors": [], "right_neighbor_id": 42811698, "left_neighbor_id": null}, "42809705": {"id": 42809705, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1469.41, "y": 302.25, "z": 11.44}, {"x": 1478.91, "y": 275.45, "z": 11.75}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1462.92, "y": 300.07, "z": 11.41}, {"x": 1462.94, "y": 300.03, "z": 11.41}, {"x": 1472.16, "y": 274.43, "z": 11.76}, {"x": 1472.51, "y": 273.46, "z": 11.78}, {"x": 1472.54, "y": 273.34, "z": 11.78}], "right_lane_mark_type": "NONE", "successors": [42809412, 42806293], "predecessors": [42806873, 42806338], "right_neighbor_id": null, "left_neighbor_id": 42808644}, "42809731": {"id": 42809731, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1440.0, "y": 203.22, "z": 12.93}, {"x": 1441.07, "y": 203.62, "z": 12.93}, {"x": 1442.23, "y": 204.12, "z": 12.93}, {"x": 1442.94, "y": 204.48, "z": 12.92}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1440.0, "y": 199.54, "z": 12.94}, {"x": 1444.15, "y": 200.94, "z": 12.9}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42808583, 42809305], "predecessors": [], "right_neighbor_id": 42807473, "left_neighbor_id": 42811963}, "42809733": {"id": 42809733, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1460.96, "y": 213.97, "z": 12.79}, {"x": 1457.25, "y": 212.64, "z": 12.82}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1459.82, "y": 217.03, "z": 12.67}, {"x": 1456.21, "y": 215.73, "z": 12.69}], "right_lane_mark_type": "NONE", "successors": [42809376, 42806507], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42809311}, "42809914": {"id": 42809914, "is_intersection": true, "lane_type": "BUS", "left_lane_boundary": [{"x": 1368.91, "y": 174.38, "z": 13.18}, {"x": 1400.28, "y": 185.29, "z": 13.07}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1370.06, "y": 170.87, "z": 13.2}, {"x": 1401.54, "y": 182.13, "z": 13.11}], "right_lane_mark_type": "NONE", "successors": [42811889], "predecessors": [42809342], "right_neighbor_id": null, "left_neighbor_id": 42808597}, "42809917": {"id": 42809917, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1419.59, "y": 195.96, "z": 13.02}, {"x": 1398.98, "y": 188.67, "z": 13.09}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1418.39, "y": 198.86, "z": 12.97}, {"x": 1397.99, "y": 191.56, "z": 13.03}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42806877, 42809419], "predecessors": [], "right_neighbor_id": 42810127, "left_neighbor_id": 42810791}, "42809918": {"id": 42809918, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1440.0, "y": 203.22, "z": 12.93}, {"x": 1419.59, "y": 195.96, "z": 13.02}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1440.0, "y": 206.61, "z": 12.89}, {"x": 1418.39, "y": 198.86, "z": 12.97}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42809917], "predecessors": [], "right_neighbor_id": 42811328, "left_neighbor_id": 42811883}, "42810127": {"id": 42810127, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1418.39, "y": 198.86, "z": 12.97}, {"x": 1397.99, "y": 191.56, "z": 13.03}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1417.09, "y": 201.83, "z": 12.86}, {"x": 1396.68, "y": 194.8, "z": 12.93}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42806291], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42809917}, "42810144": {"id": 42810144, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1603.38, "y": 253.1, "z": 12.61}, {"x": 1604.61, "y": 252.6, "z": 12.59}, {"x": 1606.35, "y": 251.98, "z": 12.57}, {"x": 1607.97, "y": 251.47, "z": 12.57}, {"x": 1610.22, "y": 250.88, "z": 12.58}, {"x": 1612.62, "y": 250.56, "z": 12.58}, {"x": 1615.0, "y": 250.31, "z": 12.58}, {"x": 1616.37, "y": 250.35, "z": 12.56}, {"x": 1617.74, "y": 250.48, "z": 12.55}, {"x": 1619.12, "y": 250.83, "z": 12.53}, {"x": 1620.43, "y": 251.35, "z": 12.51}, {"x": 1621.84, "y": 252.02, "z": 12.48}, {"x": 1622.87, "y": 252.6, "z": 12.46}, {"x": 1624.74, "y": 253.79, "z": 12.4}, {"x": 1625.83, "y": 254.72, "z": 12.37}, {"x": 1631.2, "y": 260.05, "z": 12.11}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1601.93, "y": 249.85, "z": 12.6}, {"x": 1604.39, "y": 249.03, "z": 12.62}, {"x": 1605.56, "y": 248.63, "z": 12.63}, {"x": 1607.64, "y": 248.04, "z": 12.66}, {"x": 1609.78, "y": 247.58, "z": 12.68}, {"x": 1612.18, "y": 247.08, "z": 12.7}, {"x": 1614.46, "y": 246.85, "z": 12.69}, {"x": 1617.38, "y": 247.03, "z": 12.65}, {"x": 1620.53, "y": 247.61, "z": 12.6}, {"x": 1623.06, "y": 248.54, "z": 12.55}, {"x": 1625.08, "y": 249.41, "z": 12.51}, {"x": 1626.82, "y": 250.42, "z": 12.45}, {"x": 1628.59, "y": 251.97, "z": 12.37}, {"x": 1630.16, "y": 253.68, "z": 12.31}, {"x": 1632.27, "y": 255.85, "z": 12.2}, {"x": 1633.83, "y": 257.48, "z": 12.1}], "right_lane_mark_type": "NONE", "successors": [42808634], "predecessors": [], "right_neighbor_id": 42811656, "left_neighbor_id": null}, "42810209": {"id": 42810209, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1492.05, "y": 238.97, "z": 12.28}, {"x": 1492.42, "y": 237.93, "z": 12.29}, {"x": 1492.7, "y": 236.85, "z": 12.31}, {"x": 1493.0, "y": 235.63, "z": 12.32}, {"x": 1493.22, "y": 234.25, "z": 12.36}, {"x": 1493.22, "y": 233.2, "z": 12.38}, {"x": 1493.07, "y": 232.21, "z": 12.41}, {"x": 1492.93, "y": 231.19, "z": 12.43}, {"x": 1492.67, "y": 230.28, "z": 12.45}, {"x": 1492.33, "y": 229.33, "z": 12.48}, {"x": 1491.85, "y": 228.24, "z": 12.53}, {"x": 1491.34, "y": 227.31, "z": 12.56}, {"x": 1490.88, "y": 226.61, "z": 12.58}, {"x": 1490.13, "y": 225.67, "z": 12.61}, {"x": 1489.34, "y": 224.86, "z": 12.63}, {"x": 1488.53, "y": 224.24, "z": 12.64}, {"x": 1487.81, "y": 223.78, "z": 12.65}, {"x": 1487.08, "y": 223.38, "z": 12.66}, {"x": 1486.37, "y": 223.08, "z": 12.66}, {"x": 1484.88, "y": 222.63, "z": 12.67}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1488.78, "y": 237.89, "z": 12.27}, {"x": 1489.04, "y": 237.21, "z": 12.28}, {"x": 1489.25, "y": 236.23, "z": 12.3}, {"x": 1489.29, "y": 235.06, "z": 12.32}, {"x": 1489.28, "y": 233.89, "z": 12.35}, {"x": 1489.24, "y": 232.95, "z": 12.38}, {"x": 1489.14, "y": 232.09, "z": 12.39}, {"x": 1489.02, "y": 231.29, "z": 12.42}, {"x": 1488.84, "y": 230.53, "z": 12.43}, {"x": 1488.43, "y": 229.64, "z": 12.45}, {"x": 1488.14, "y": 229.0, "z": 12.46}, {"x": 1487.65, "y": 228.33, "z": 12.47}, {"x": 1487.05, "y": 227.62, "z": 12.49}, {"x": 1486.42, "y": 227.03, "z": 12.5}, {"x": 1485.71, "y": 226.43, "z": 12.52}, {"x": 1484.81, "y": 225.98, "z": 12.53}, {"x": 1483.78, "y": 225.64, "z": 12.53}], "right_lane_mark_type": "NONE", "successors": [42807335], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42811684}, "42810413": {"id": 42810413, "is_intersection": false, "lane_type": "BUS", "left_lane_boundary": [{"x": 1444.15, "y": 200.94, "z": 12.9}, {"x": 1459.46, "y": 206.46, "z": 12.82}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1445.2, "y": 197.77, "z": 12.92}, {"x": 1460.63, "y": 203.29, "z": 12.83}], "right_lane_mark_type": "NONE", "successors": [42809309], "predecessors": [42807473], "right_neighbor_id": null, "left_neighbor_id": 42809305}, "42810437": {"id": 42810437, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1454.58, "y": 306.44, "z": 11.49}, {"x": 1481.38, "y": 316.09, "z": 11.26}], "left_lane_mark_type": "DASHED_YELLOW", "right_lane_boundary": [{"x": 1455.03, "y": 305.07, "z": 11.51}, {"x": 1455.96, "y": 305.4, "z": 11.54}, {"x": 1456.13, "y": 305.47, "z": 11.53}, {"x": 1456.77, "y": 305.71, "z": 11.5}, {"x": 1456.88, "y": 305.73, "z": 11.49}, {"x": 1481.97, "y": 314.63, "z": 11.28}], "right_lane_mark_type": "NONE", "successors": [42808752], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42810822}, "42810749": {"id": 42810749, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1539.06, "y": 241.53, "z": 12.53}, {"x": 1536.84, "y": 240.41, "z": 12.55}, {"x": 1526.76, "y": 234.44, "z": 12.62}, {"x": 1524.05, "y": 233.18, "z": 12.62}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1537.75, "y": 244.5, "z": 12.41}, {"x": 1535.79, "y": 243.4, "z": 12.43}, {"x": 1525.46, "y": 236.98, "z": 12.55}, {"x": 1523.15, "y": 235.93, "z": 12.56}], "right_lane_mark_type": "NONE", "successors": [42811338], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42811282}, "42810750": {"id": 42810750, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1523.15, "y": 235.93, "z": 12.56}, {"x": 1509.06, "y": 231.1, "z": 12.57}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1522.01, "y": 239.05, "z": 12.41}, {"x": 1507.87, "y": 234.12, "z": 12.39}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42806420], "predecessors": [], "right_neighbor_id": 42811290, "left_neighbor_id": 42811338}, "42810767": {"id": 42810767, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1482.45, "y": 265.91, "z": 11.89}, {"x": 1478.91, "y": 275.45, "z": 11.75}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1485.5, "y": 266.88, "z": 11.81}, {"x": 1482.11, "y": 276.35, "z": 11.67}], "right_lane_mark_type": "NONE", "successors": [42808644], "predecessors": [], "right_neighbor_id": 42808642, "left_neighbor_id": 42809412}, "42810769": {"id": 42810769, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1477.63, "y": 220.04, "z": 12.71}, {"x": 1460.96, "y": 213.97, "z": 12.79}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1476.59, "y": 223.05, "z": 12.57}, {"x": 1459.82, "y": 217.03, "z": 12.67}], "right_lane_mark_type": "NONE", "successors": [42809733], "predecessors": [42807335], "right_neighbor_id": null, "left_neighbor_id": 42811445}, "42810779": {"id": 42810779, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1442.01, "y": 207.33, "z": 12.9}, {"x": 1440.0, "y": 206.61, "z": 12.89}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1441.06, "y": 210.32, "z": 12.77}, {"x": 1440.0, "y": 210.01, "z": 12.76}], "right_lane_mark_type": "NONE", "successors": [42811328], "predecessors": [42806507], "right_neighbor_id": null, "left_neighbor_id": 42811963}, "42810791": {"id": 42810791, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1398.98, "y": 188.67, "z": 13.09}, {"x": 1419.59, "y": 195.96, "z": 13.02}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1400.28, "y": 185.29, "z": 13.07}, {"x": 1420.96, "y": 192.74, "z": 13.01}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42811883], "predecessors": [42806529, 42808597], "right_neighbor_id": 42811889, "left_neighbor_id": 42809917}, "42810795": {"id": 42810795, "is_intersection": true, "lane_type": "BUS", "left_lane_boundary": [{"x": 1487.16, "y": 216.4, "z": 12.69}, {"x": 1511.26, "y": 225.25, "z": 12.58}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1488.27, "y": 213.4, "z": 12.68}, {"x": 1512.4, "y": 221.74, "z": 12.6}], "right_lane_mark_type": "NONE", "successors": [42811280], "predecessors": [42808620], "right_neighbor_id": null, "left_neighbor_id": 42809424}, "42810822": {"id": 42810822, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1481.38, "y": 316.09, "z": 11.26}, {"x": 1454.58, "y": 306.44, "z": 11.49}], "left_lane_mark_type": "DASHED_YELLOW", "right_lane_boundary": [{"x": 1480.93, "y": 317.3, "z": 11.27}, {"x": 1477.36, "y": 316.06, "z": 11.29}, {"x": 1477.34, "y": 316.06, "z": 11.29}, {"x": 1453.98, "y": 307.86, "z": 11.51}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42808987], "predecessors": [42808753], "right_neighbor_id": null, "left_neighbor_id": 42810437}, "42810823": {"id": 42810823, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1440.0, "y": 301.22, "z": 11.7}, {"x": 1454.58, "y": 306.44, "z": 11.49}], "left_lane_mark_type": "DASHED_YELLOW", "right_lane_boundary": [{"x": 1440.0, "y": 299.62, "z": 11.73}, {"x": 1444.06, "y": 301.0, "z": 11.69}, {"x": 1451.8, "y": 303.79, "z": 11.57}, {"x": 1455.03, "y": 305.07, "z": 11.51}], "right_lane_mark_type": "NONE", "successors": [42817816, 42810437, 42817773], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42808987}, "42810830": {"id": 42810830, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1590.0, "y": 257.68, "z": 12.56}, {"x": 1588.36, "y": 257.68, "z": 12.57}, {"x": 1586.7, "y": 257.52, "z": 12.58}, {"x": 1583.88, "y": 257.07, "z": 12.53}, {"x": 1581.47, "y": 256.42, "z": 12.53}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 1590.0, "y": 261.18, "z": 12.54}, {"x": 1585.73, "y": 260.86, "z": 12.53}, {"x": 1584.96, "y": 260.71, "z": 12.53}, {"x": 1581.41, "y": 259.88, "z": 12.51}, {"x": 1580.4, "y": 259.57, "z": 12.52}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42915647], "predecessors": [], "right_neighbor_id": 42811882, "left_neighbor_id": null}, "42810833": {"id": 42810833, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1491.68, "y": 240.0, "z": 12.26}, {"x": 1492.05, "y": 238.97, "z": 12.28}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1487.99, "y": 240.0, "z": 12.24}, {"x": 1488.78, "y": 237.89, "z": 12.27}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42810209, 42807745, 42807471], "predecessors": [42809413], "right_neighbor_id": null, "left_neighbor_id": 42810834}, "42810834": {"id": 42810834, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1492.05, "y": 238.97, "z": 12.28}, {"x": 1491.68, "y": 240.0, "z": 12.26}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1495.48, "y": 239.66, "z": 12.18}, {"x": 1495.36, "y": 240.0, "z": 12.17}], "right_lane_mark_type": "NONE", "successors": [42811679], "predecessors": [42806933], "right_neighbor_id": 42811961, "left_neighbor_id": 42810833}, "42810978": {"id": 42810978, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1440.0, "y": 307.04, "z": 11.65}, {"x": 1436.5, "y": 305.78, "z": 11.69}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1440.0, "y": 310.43, "z": 11.6}, {"x": 1435.44, "y": 308.81, "z": 11.69}], "right_lane_mark_type": "NONE", "successors": [42811454], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42808948}, "42811247": {"id": 42811247, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1422.05, "y": 294.85, "z": 11.76}, {"x": 1426.94, "y": 296.59, "z": 11.73}], "left_lane_mark_type": "DASHED_YELLOW", "right_lane_boundary": [{"x": 1422.53, "y": 293.39, "z": 11.8}, {"x": 1427.46, "y": 295.14, "z": 11.77}], "right_lane_mark_type": "NONE", "successors": [42811479], "predecessors": [42808440], "right_neighbor_id": null, "left_neighbor_id": 42811311}, "42811275": {"id": 42811275, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1539.06, "y": 241.53, "z": 12.53}, {"x": 1523.15, "y": 235.93, "z": 12.56}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1537.75, "y": 244.5, "z": 12.41}, {"x": 1522.01, "y": 239.05, "z": 12.41}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42810750], "predecessors": [], "right_neighbor_id": 42811446, "left_neighbor_id": null}, "42811280": {"id": 42811280, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1511.26, "y": 225.25, "z": 12.58}, {"x": 1525.14, "y": 230.26, "z": 12.58}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1512.4, "y": 221.74, "z": 12.6}, {"x": 1526.43, "y": 226.7, "z": 12.55}], "right_lane_mark_type": "NONE", "successors": [42809321], "predecessors": [42810795, 42807644], "right_neighbor_id": null, "left_neighbor_id": 42811495}, "42811281": {"id": 42811281, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1524.05, "y": 233.18, "z": 12.62}, {"x": 1540.03, "y": 238.45, "z": 12.57}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1525.14, "y": 230.26, "z": 12.58}, {"x": 1541.07, "y": 235.59, "z": 12.54}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42811505], "predecessors": [], "right_neighbor_id": 42809321, "left_neighbor_id": null}, "42811282": {"id": 42811282, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1524.05, "y": 233.18, "z": 12.62}, {"x": 1526.76, "y": 234.44, "z": 12.62}, {"x": 1536.84, "y": 240.41, "z": 12.55}, {"x": 1539.06, "y": 241.53, "z": 12.53}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1525.14, "y": 230.26, "z": 12.58}, {"x": 1526.77, "y": 230.99, "z": 12.58}, {"x": 1538.59, "y": 237.77, "z": 12.57}, {"x": 1540.03, "y": 238.45, "z": 12.57}], "right_lane_mark_type": "NONE", "successors": [42811503], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42810749}, "42811283": {"id": 42811283, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1577.64, "y": 255.23, "z": 12.55}, {"x": 1554.63, "y": 247.07, "z": 12.51}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1576.57, "y": 258.32, "z": 12.49}, {"x": 1564.6, "y": 254.1, "z": 12.42}, {"x": 1553.63, "y": 250.12, "z": 12.41}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42811319], "predecessors": [], "right_neighbor_id": 42811323, "left_neighbor_id": 42811288}, "42811286": {"id": 42811286, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1477.63, "y": 220.04, "z": 12.71}, {"x": 1484.88, "y": 222.63, "z": 12.67}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1478.77, "y": 216.84, "z": 12.77}, {"x": 1486.03, "y": 219.35, "z": 12.73}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42811684], "predecessors": [], "right_neighbor_id": 42811322, "left_neighbor_id": 42807335}, "42811288": {"id": 42811288, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1554.63, "y": 247.07, "z": 12.51}, {"x": 1577.64, "y": 255.23, "z": 12.55}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1555.7, "y": 244.11, "z": 12.53}, {"x": 1573.25, "y": 250.34, "z": 12.53}, {"x": 1575.71, "y": 251.04, "z": 12.53}, {"x": 1578.37, "y": 251.64, "z": 12.53}, {"x": 1578.81, "y": 251.78, "z": 12.53}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42807643], "predecessors": [], "right_neighbor_id": 42811335, "left_neighbor_id": 42811283}, "42811290": {"id": 42811290, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1522.01, "y": 239.05, "z": 12.41}, {"x": 1507.87, "y": 234.12, "z": 12.39}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1520.78, "y": 242.37, "z": 12.3}, {"x": 1507.21, "y": 237.51, "z": 12.27}, {"x": 1506.47, "y": 237.27, "z": 12.24}], "right_lane_mark_type": "NONE", "successors": [42806684], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42810750}, "42811311": {"id": 42811311, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1426.94, "y": 296.59, "z": 11.73}, {"x": 1422.05, "y": 294.85, "z": 11.76}], "left_lane_mark_type": "DASHED_YELLOW", "right_lane_boundary": [{"x": 1426.42, "y": 298.06, "z": 11.76}, {"x": 1421.53, "y": 296.32, "z": 11.78}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42811879], "predecessors": [42808559], "right_neighbor_id": null, "left_neighbor_id": 42811247}, "42811319": {"id": 42811319, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1554.63, "y": 247.07, "z": 12.51}, {"x": 1545.02, "y": 243.65, "z": 12.52}, {"x": 1539.06, "y": 241.53, "z": 12.53}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1553.63, "y": 250.12, "z": 12.41}, {"x": 1544.36, "y": 246.94, "z": 12.4}, {"x": 1537.75, "y": 244.5, "z": 12.41}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42810749, 42811275], "predecessors": [42811283], "right_neighbor_id": 42811488, "left_neighbor_id": 42811503}, "42811322": {"id": 42811322, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1478.77, "y": 216.84, "z": 12.77}, {"x": 1486.03, "y": 219.35, "z": 12.73}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1479.88, "y": 213.79, "z": 12.72}, {"x": 1487.16, "y": 216.4, "z": 12.69}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42809424], "predecessors": [], "right_neighbor_id": 42808620, "left_neighbor_id": 42811286}, "42811323": {"id": 42811323, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1576.57, "y": 258.32, "z": 12.49}, {"x": 1564.6, "y": 254.1, "z": 12.42}, {"x": 1553.63, "y": 250.12, "z": 12.41}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1575.47, "y": 261.72, "z": 12.46}, {"x": 1572.46, "y": 260.64, "z": 12.45}, {"x": 1565.27, "y": 258.26, "z": 12.36}, {"x": 1552.19, "y": 253.56, "z": 12.33}], "right_lane_mark_type": "NONE", "successors": [42811488], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42811283}, "42811328": {"id": 42811328, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1440.0, "y": 206.61, "z": 12.89}, {"x": 1418.39, "y": 198.86, "z": 12.97}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1440.0, "y": 210.01, "z": 12.76}, {"x": 1417.09, "y": 201.83, "z": 12.86}], "right_lane_mark_type": "NONE", "successors": [42810127], "predecessors": [42810779], "right_neighbor_id": null, "left_neighbor_id": 42809918}, "42811329": {"id": 42811329, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1502.42, "y": 210.24, "z": 12.7}, {"x": 1502.67, "y": 208.89, "z": 12.73}, {"x": 1502.89, "y": 206.54, "z": 12.77}, {"x": 1503.05, "y": 204.2, "z": 12.82}, {"x": 1503.37, "y": 200.7, "z": 12.89}, {"x": 1503.53, "y": 197.95, "z": 12.95}, {"x": 1503.59, "y": 195.75, "z": 12.98}, {"x": 1503.55, "y": 193.55, "z": 13.0}, {"x": 1503.36, "y": 191.95, "z": 13.02}, {"x": 1502.82, "y": 190.55, "z": 13.04}, {"x": 1498.49, "y": 181.53, "z": 13.24}, {"x": 1493.25, "y": 171.06, "z": 13.51}, {"x": 1467.39, "y": 120.0, "z": 14.99}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1496.78, "y": 208.12, "z": 12.74}, {"x": 1497.08, "y": 207.09, "z": 12.76}, {"x": 1498.3, "y": 203.08, "z": 12.89}, {"x": 1498.57, "y": 202.08, "z": 12.89}, {"x": 1498.68, "y": 201.79, "z": 12.89}, {"x": 1498.98, "y": 200.66, "z": 12.9}, {"x": 1499.18, "y": 199.59, "z": 12.92}, {"x": 1499.17, "y": 199.37, "z": 12.93}, {"x": 1499.25, "y": 198.68, "z": 12.94}, {"x": 1499.16, "y": 197.38, "z": 12.96}, {"x": 1498.63, "y": 195.31, "z": 13.02}, {"x": 1497.16, "y": 192.5, "z": 13.08}, {"x": 1495.31, "y": 188.96, "z": 13.2}, {"x": 1460.72, "y": 120.0, "z": 15.09}], "right_lane_mark_type": "NONE", "successors": [42816397], "predecessors": [42807745, 42806682, 42806422], "right_neighbor_id": null, "left_neighbor_id": 42811989}, "42811335": {"id": 42811335, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1555.7, "y": 244.11, "z": 12.53}, {"x": 1573.25, "y": 250.34, "z": 12.53}, {"x": 1575.71, "y": 251.04, "z": 12.53}, {"x": 1578.37, "y": 251.64, "z": 12.53}, {"x": 1578.81, "y": 251.78, "z": 12.53}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1556.83, "y": 241.15, "z": 12.5}, {"x": 1575.59, "y": 247.65, "z": 12.48}, {"x": 1579.26, "y": 248.61, "z": 12.5}, {"x": 1579.93, "y": 248.81, "z": 12.5}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42812483], "predecessors": [], "right_neighbor_id": 42811491, "left_neighbor_id": 42811288}, "42811336": {"id": 42811336, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1590.0, "y": 252.59, "z": 12.54}, {"x": 1593.35, "y": 252.2, "z": 12.57}, {"x": 1595.66, "y": 251.68, "z": 12.58}, {"x": 1598.52, "y": 250.95, "z": 12.59}, {"x": 1601.07, "y": 250.11, "z": 12.59}, {"x": 1601.93, "y": 249.85, "z": 12.6}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1590.0, "y": 249.27, "z": 12.54}, {"x": 1594.69, "y": 248.47, "z": 12.57}, {"x": 1597.52, "y": 247.75, "z": 12.59}, {"x": 1600.38, "y": 246.57, "z": 12.62}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42811656, 42819897], "predecessors": [], "right_neighbor_id": 42811683, "left_neighbor_id": 42811541}, "42811338": {"id": 42811338, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1524.05, "y": 233.18, "z": 12.62}, {"x": 1517.89, "y": 230.77, "z": 12.63}, {"x": 1510.24, "y": 228.05, "z": 12.62}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1523.15, "y": 235.93, "z": 12.56}, {"x": 1509.06, "y": 231.1, "z": 12.57}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42806682], "predecessors": [42810749], "right_neighbor_id": 42810750, "left_neighbor_id": 42811495}, "42811445": {"id": 42811445, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1460.96, "y": 213.97, "z": 12.79}, {"x": 1477.63, "y": 220.04, "z": 12.71}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1462.12, "y": 210.95, "z": 12.84}, {"x": 1464.03, "y": 211.6, "z": 12.83}, {"x": 1478.77, "y": 216.84, "z": 12.77}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42811286], "predecessors": [42809311], "right_neighbor_id": 42811487, "left_neighbor_id": 42810769}, "42811446": {"id": 42811446, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1537.75, "y": 244.5, "z": 12.41}, {"x": 1522.01, "y": 239.05, "z": 12.41}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1536.42, "y": 247.96, "z": 12.29}, {"x": 1520.78, "y": 242.37, "z": 12.3}], "right_lane_mark_type": "NONE", "successors": [42811290], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42811275}, "42811454": {"id": 42811454, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1436.5, "y": 305.78, "z": 11.69}, {"x": 1430.9, "y": 303.85, "z": 11.72}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1435.44, "y": 308.81, "z": 11.69}, {"x": 1429.9, "y": 306.83, "z": 11.67}], "right_lane_mark_type": "NONE", "successors": [42806889], "predecessors": [42810978], "right_neighbor_id": null, "left_neighbor_id": 42811456}, "42811456": {"id": 42811456, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1437.58, "y": 302.7, "z": 11.72}, {"x": 1431.97, "y": 300.7, "z": 11.76}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1436.5, "y": 305.78, "z": 11.69}, {"x": 1430.9, "y": 303.85, "z": 11.72}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42809667], "predecessors": [42808948], "right_neighbor_id": 42811454, "left_neighbor_id": null}, "42811457": {"id": 42811457, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1479.55, "y": 321.04, "z": 11.23}, {"x": 1452.85, "y": 311.64, "z": 11.44}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1478.34, "y": 324.43, "z": 11.23}, {"x": 1453.09, "y": 315.11, "z": 11.35}, {"x": 1452.97, "y": 315.05, "z": 11.35}, {"x": 1452.45, "y": 314.84, "z": 11.39}, {"x": 1451.77, "y": 314.62, "z": 11.41}], "right_lane_mark_type": "NONE", "successors": [42811474], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42808012}, "42811474": {"id": 42811474, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1452.85, "y": 311.64, "z": 11.44}, {"x": 1440.0, "y": 307.04, "z": 11.65}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1451.77, "y": 314.62, "z": 11.41}, {"x": 1440.0, "y": 310.43, "z": 11.6}], "right_lane_mark_type": "NONE", "successors": [42810978], "predecessors": [42811457, 42807547], "right_neighbor_id": null, "left_neighbor_id": 42809364}, "42811479": {"id": 42811479, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1426.94, "y": 296.59, "z": 11.73}, {"x": 1440.0, "y": 301.22, "z": 11.7}], "left_lane_mark_type": "DASHED_YELLOW", "right_lane_boundary": [{"x": 1427.46, "y": 295.14, "z": 11.77}, {"x": 1427.5, "y": 295.17, "z": 11.78}, {"x": 1440.0, "y": 299.62, "z": 11.73}], "right_lane_mark_type": "NONE", "successors": [42810823], "predecessors": [42811247], "right_neighbor_id": null, "left_neighbor_id": 42808559}, "42811487": {"id": 42811487, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1462.12, "y": 210.95, "z": 12.84}, {"x": 1464.03, "y": 211.6, "z": 12.83}, {"x": 1478.77, "y": 216.84, "z": 12.77}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1463.29, "y": 207.87, "z": 12.81}, {"x": 1479.88, "y": 213.79, "z": 12.72}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42811322], "predecessors": [42809307], "right_neighbor_id": 42806907, "left_neighbor_id": 42811445}, "42811488": {"id": 42811488, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1553.63, "y": 250.12, "z": 12.41}, {"x": 1544.36, "y": 246.94, "z": 12.4}, {"x": 1537.75, "y": 244.5, "z": 12.41}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1552.19, "y": 253.56, "z": 12.33}, {"x": 1536.42, "y": 247.96, "z": 12.29}], "right_lane_mark_type": "NONE", "successors": [42811446], "predecessors": [42811323], "right_neighbor_id": null, "left_neighbor_id": 42811319}, "42811491": {"id": 42811491, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1556.83, "y": 241.15, "z": 12.5}, {"x": 1575.59, "y": 247.65, "z": 12.48}, {"x": 1579.26, "y": 248.61, "z": 12.5}, {"x": 1579.93, "y": 248.81, "z": 12.5}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1558.05, "y": 237.87, "z": 12.47}, {"x": 1576.94, "y": 244.59, "z": 12.47}, {"x": 1577.95, "y": 244.84, "z": 12.46}, {"x": 1579.26, "y": 245.24, "z": 12.47}, {"x": 1581.06, "y": 245.64, "z": 12.48}], "right_lane_mark_type": "NONE", "successors": [42812494], "predecessors": [42809329], "right_neighbor_id": null, "left_neighbor_id": 42811335}, "42811495": {"id": 42811495, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1510.24, "y": 228.05, "z": 12.62}, {"x": 1517.89, "y": 230.77, "z": 12.63}, {"x": 1524.05, "y": 233.18, "z": 12.62}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1511.26, "y": 225.25, "z": 12.58}, {"x": 1525.14, "y": 230.26, "z": 12.58}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42811282, 42811281], "predecessors": [42809424, 42807471], "right_neighbor_id": 42811280, "left_neighbor_id": 42811338}, "42811503": {"id": 42811503, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1539.06, "y": 241.53, "z": 12.53}, {"x": 1545.02, "y": 243.65, "z": 12.52}, {"x": 1554.63, "y": 247.07, "z": 12.51}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1540.03, "y": 238.45, "z": 12.57}, {"x": 1555.7, "y": 244.11, "z": 12.53}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42811288], "predecessors": [42811282], "right_neighbor_id": 42811505, "left_neighbor_id": 42811319}, "42811505": {"id": 42811505, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1540.03, "y": 238.45, "z": 12.57}, {"x": 1555.7, "y": 244.11, "z": 12.53}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1541.07, "y": 235.59, "z": 12.54}, {"x": 1556.83, "y": 241.15, "z": 12.5}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42811335], "predecessors": [42811281], "right_neighbor_id": 42809329, "left_neighbor_id": 42811503}, "42811541": {"id": 42811541, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1590.0, "y": 255.85, "z": 12.58}, {"x": 1592.84, "y": 255.47, "z": 12.6}, {"x": 1596.0, "y": 255.06, "z": 12.57}, {"x": 1598.0, "y": 254.62, "z": 12.64}, {"x": 1599.59, "y": 254.25, "z": 12.61}, {"x": 1600.74, "y": 253.94, "z": 12.62}, {"x": 1601.88, "y": 253.6, "z": 12.64}, {"x": 1603.38, "y": 253.1, "z": 12.61}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 1590.0, "y": 252.59, "z": 12.54}, {"x": 1593.35, "y": 252.2, "z": 12.57}, {"x": 1595.66, "y": 251.68, "z": 12.58}, {"x": 1598.52, "y": 250.95, "z": 12.59}, {"x": 1601.07, "y": 250.11, "z": 12.59}, {"x": 1601.93, "y": 249.85, "z": 12.6}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42810144, 42807339], "predecessors": [], "right_neighbor_id": 42811336, "left_neighbor_id": null}, "42811650": {"id": 42811650, "is_intersection": false, "lane_type": "BUS", "left_lane_boundary": [{"x": 1420.96, "y": 192.74, "z": 13.01}, {"x": 1440.0, "y": 199.54, "z": 12.94}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1422.21, "y": 189.55, "z": 13.03}, {"x": 1431.82, "y": 192.95, "z": 12.99}, {"x": 1437.09, "y": 194.77, "z": 12.96}, {"x": 1440.0, "y": 195.91, "z": 12.95}], "right_lane_mark_type": "NONE", "successors": [42807473], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42811883}, "42811656": {"id": 42811656, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1601.93, "y": 249.85, "z": 12.6}, {"x": 1604.39, "y": 249.03, "z": 12.62}, {"x": 1605.56, "y": 248.63, "z": 12.63}, {"x": 1607.64, "y": 248.04, "z": 12.66}, {"x": 1609.78, "y": 247.58, "z": 12.68}, {"x": 1612.18, "y": 247.08, "z": 12.7}, {"x": 1614.46, "y": 246.85, "z": 12.69}, {"x": 1617.38, "y": 247.03, "z": 12.65}, {"x": 1620.53, "y": 247.61, "z": 12.6}, {"x": 1623.06, "y": 248.54, "z": 12.55}, {"x": 1625.08, "y": 249.41, "z": 12.51}, {"x": 1626.82, "y": 250.42, "z": 12.45}, {"x": 1628.59, "y": 251.97, "z": 12.37}, {"x": 1630.16, "y": 253.68, "z": 12.31}, {"x": 1632.27, "y": 255.85, "z": 12.2}, {"x": 1633.83, "y": 257.48, "z": 12.1}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1600.38, "y": 246.57, "z": 12.62}, {"x": 1603.83, "y": 245.33, "z": 12.68}, {"x": 1607.57, "y": 244.51, "z": 12.75}, {"x": 1611.79, "y": 243.88, "z": 12.78}, {"x": 1614.84, "y": 243.71, "z": 12.77}, {"x": 1618.7, "y": 243.96, "z": 12.72}, {"x": 1622.21, "y": 244.6, "z": 12.67}, {"x": 1625.44, "y": 245.76, "z": 12.6}, {"x": 1628.13, "y": 247.25, "z": 12.51}, {"x": 1630.42, "y": 249.07, "z": 12.41}, {"x": 1632.29, "y": 250.84, "z": 12.31}, {"x": 1636.27, "y": 255.11, "z": 12.14}], "right_lane_mark_type": "NONE", "successors": [42812446], "predecessors": [42811336], "right_neighbor_id": null, "left_neighbor_id": 42810144}, "42811679": {"id": 42811679, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1491.68, "y": 240.0, "z": 12.26}, {"x": 1482.45, "y": 265.91, "z": 11.89}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1495.36, "y": 240.0, "z": 12.17}, {"x": 1492.93, "y": 246.98, "z": 12.07}, {"x": 1485.5, "y": 266.88, "z": 11.81}], "right_lane_mark_type": "NONE", "successors": [42806926, 42810767], "predecessors": [42810834], "right_neighbor_id": 42808745, "left_neighbor_id": 42809413}, "42811683": {"id": 42811683, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1590.0, "y": 249.27, "z": 12.54}, {"x": 1594.69, "y": 248.47, "z": 12.57}, {"x": 1597.52, "y": 247.75, "z": 12.59}, {"x": 1600.38, "y": 246.57, "z": 12.62}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1590.0, "y": 245.99, "z": 12.5}, {"x": 1592.09, "y": 245.87, "z": 12.55}, {"x": 1593.9, "y": 245.45, "z": 12.58}, {"x": 1598.89, "y": 243.49, "z": 12.65}], "right_lane_mark_type": "NONE", "successors": [42807784, 42807338], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42811336}, "42811684": {"id": 42811684, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1484.88, "y": 222.63, "z": 12.67}, {"x": 1486.37, "y": 223.08, "z": 12.66}, {"x": 1487.08, "y": 223.38, "z": 12.66}, {"x": 1487.81, "y": 223.78, "z": 12.65}, {"x": 1488.53, "y": 224.24, "z": 12.64}, {"x": 1489.34, "y": 224.86, "z": 12.63}, {"x": 1490.13, "y": 225.67, "z": 12.61}, {"x": 1490.88, "y": 226.61, "z": 12.58}, {"x": 1491.34, "y": 227.31, "z": 12.56}, {"x": 1491.85, "y": 228.24, "z": 12.53}, {"x": 1492.33, "y": 229.33, "z": 12.48}, {"x": 1492.67, "y": 230.28, "z": 12.45}, {"x": 1492.93, "y": 231.19, "z": 12.43}, {"x": 1493.07, "y": 232.21, "z": 12.41}, {"x": 1493.22, "y": 233.2, "z": 12.38}, {"x": 1493.22, "y": 234.25, "z": 12.36}, {"x": 1493.0, "y": 235.63, "z": 12.32}, {"x": 1492.7, "y": 236.85, "z": 12.31}, {"x": 1492.42, "y": 237.93, "z": 12.29}, {"x": 1492.05, "y": 238.97, "z": 12.28}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1486.03, "y": 219.35, "z": 12.73}, {"x": 1487.95, "y": 220.04, "z": 12.71}, {"x": 1488.87, "y": 220.53, "z": 12.7}, {"x": 1489.85, "y": 221.13, "z": 12.7}, {"x": 1490.8, "y": 221.83, "z": 12.7}, {"x": 1492.02, "y": 222.78, "z": 12.7}, {"x": 1492.88, "y": 223.68, "z": 12.7}, {"x": 1493.45, "y": 224.36, "z": 12.7}, {"x": 1494.05, "y": 225.11, "z": 12.69}, {"x": 1494.64, "y": 225.98, "z": 12.65}, {"x": 1495.16, "y": 226.98, "z": 12.61}, {"x": 1495.63, "y": 228.17, "z": 12.56}, {"x": 1495.88, "y": 229.17, "z": 12.53}, {"x": 1496.2, "y": 230.15, "z": 12.49}, {"x": 1496.52, "y": 231.2, "z": 12.43}, {"x": 1496.61, "y": 232.3, "z": 12.38}, {"x": 1496.53, "y": 233.64, "z": 12.32}, {"x": 1496.32, "y": 235.53, "z": 12.28}, {"x": 1496.01, "y": 237.12, "z": 12.24}, {"x": 1495.76, "y": 238.16, "z": 12.22}, {"x": 1495.55, "y": 239.12, "z": 12.2}, {"x": 1495.48, "y": 239.66, "z": 12.18}], "right_lane_mark_type": "NONE", "successors": [42810834], "predecessors": [42811286], "right_neighbor_id": null, "left_neighbor_id": 42810209}, "42811689": {"id": 42811689, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1604.15, "y": 255.02, "z": 12.55}, {"x": 1602.34, "y": 255.74, "z": 12.58}, {"x": 1598.78, "y": 256.56, "z": 12.6}, {"x": 1596.21, "y": 257.03, "z": 12.57}, {"x": 1593.06, "y": 257.51, "z": 12.59}, {"x": 1590.0, "y": 257.68, "z": 12.56}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 1605.35, "y": 258.22, "z": 12.45}, {"x": 1602.5, "y": 259.28, "z": 12.49}, {"x": 1600.01, "y": 260.07, "z": 12.52}, {"x": 1596.42, "y": 260.76, "z": 12.53}, {"x": 1594.13, "y": 260.96, "z": 12.54}, {"x": 1591.76, "y": 261.07, "z": 12.54}, {"x": 1590.0, "y": 261.18, "z": 12.54}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42810830], "predecessors": [42807548], "right_neighbor_id": 42812480, "left_neighbor_id": null}, "42811698": {"id": 42811698, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1511.09, "y": 332.36, "z": 11.36}, {"x": 1479.55, "y": 321.04, "z": 11.23}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1509.96, "y": 335.6, "z": 11.41}, {"x": 1509.53, "y": 335.45, "z": 11.37}, {"x": 1496.98, "y": 331.0, "z": 11.21}, {"x": 1496.09, "y": 330.68, "z": 11.29}, {"x": 1496.03, "y": 330.62, "z": 11.28}, {"x": 1482.07, "y": 325.71, "z": 11.26}, {"x": 1478.34, "y": 324.43, "z": 11.23}], "right_lane_mark_type": "NONE", "successors": [42806871, 42811457], "predecessors": [42811307], "right_neighbor_id": null, "left_neighbor_id": 42809703}, "42811879": {"id": 42811879, "is_intersection": false, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1422.05, "y": 294.85, "z": 11.76}, {"x": 1350.0, "y": 269.27, "z": 12.13}], "left_lane_mark_type": "DASHED_YELLOW", "right_lane_boundary": [{"x": 1421.53, "y": 296.32, "z": 11.78}, {"x": 1413.87, "y": 293.53, "z": 11.8}, {"x": 1413.86, "y": 293.52, "z": 11.8}, {"x": 1413.47, "y": 293.38, "z": 11.81}, {"x": 1413.37, "y": 293.35, "z": 11.81}, {"x": 1350.0, "y": 270.81, "z": 12.15}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42809363], "predecessors": [42811311], "right_neighbor_id": null, "left_neighbor_id": 42808440}, "42811882": {"id": 42811882, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1590.0, "y": 261.18, "z": 12.54}, {"x": 1585.73, "y": 260.86, "z": 12.53}, {"x": 1584.96, "y": 260.71, "z": 12.53}, {"x": 1581.41, "y": 259.88, "z": 12.51}, {"x": 1580.4, "y": 259.57, "z": 12.52}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1590.0, "y": 264.73, "z": 12.52}, {"x": 1586.22, "y": 264.53, "z": 12.55}, {"x": 1581.71, "y": 263.7, "z": 12.52}, {"x": 1579.09, "y": 262.87, "z": 12.45}], "right_lane_mark_type": "NONE", "successors": [42915541], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42810830}, "42811883": {"id": 42811883, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1419.59, "y": 195.96, "z": 13.02}, {"x": 1440.0, "y": 203.22, "z": 12.93}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1420.96, "y": 192.74, "z": 13.01}, {"x": 1440.0, "y": 199.54, "z": 12.94}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42809731], "predecessors": [42810791], "right_neighbor_id": 42811650, "left_neighbor_id": 42809918}, "42811887": {"id": 42811887, "is_intersection": false, "lane_type": "BUS", "left_lane_boundary": [{"x": 1382.0, "y": 159.55, "z": 13.74}, {"x": 1386.01, "y": 167.54, "z": 13.52}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1384.98, "y": 158.11, "z": 13.75}, {"x": 1389.06, "y": 166.15, "z": 13.57}], "right_lane_mark_type": "NONE", "successors": [42806529], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42806903}, "42811889": {"id": 42811889, "is_intersection": false, "lane_type": "BUS", "left_lane_boundary": [{"x": 1400.28, "y": 185.29, "z": 13.07}, {"x": 1420.96, "y": 192.74, "z": 13.01}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1401.54, "y": 182.13, "z": 13.11}, {"x": 1403.86, "y": 182.96, "z": 13.06}, {"x": 1406.41, "y": 183.91, "z": 13.07}, {"x": 1410.73, "y": 185.43, "z": 13.1}, {"x": 1414.9, "y": 186.92, "z": 13.02}, {"x": 1417.58, "y": 187.91, "z": 13.02}, {"x": 1422.21, "y": 189.55, "z": 13.03}], "right_lane_mark_type": "NONE", "successors": [42811650], "predecessors": [42809914], "right_neighbor_id": null, "left_neighbor_id": 42810791}, "42811961": {"id": 42811961, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1495.48, "y": 239.66, "z": 12.18}, {"x": 1495.36, "y": 240.0, "z": 12.17}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1498.46, "y": 239.86, "z": 12.18}, {"x": 1498.41, "y": 240.0, "z": 12.16}], "right_lane_mark_type": "NONE", "successors": [42808745], "predecessors": [42806288, 42806684], "right_neighbor_id": null, "left_neighbor_id": 42810834}, "42811963": {"id": 42811963, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1442.94, "y": 204.48, "z": 12.92}, {"x": 1442.23, "y": 204.12, "z": 12.93}, {"x": 1441.07, "y": 203.62, "z": 12.93}, {"x": 1440.0, "y": 203.22, "z": 12.93}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1442.01, "y": 207.33, "z": 12.9}, {"x": 1440.0, "y": 206.61, "z": 12.89}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42809918], "predecessors": [42809376], "right_neighbor_id": 42810779, "left_neighbor_id": 42809731}, "42811989": {"id": 42811989, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1467.39, "y": 120.0, "z": 14.99}, {"x": 1493.25, "y": 171.06, "z": 13.51}, {"x": 1498.49, "y": 181.53, "z": 13.24}, {"x": 1502.82, "y": 190.55, "z": 13.04}, {"x": 1503.36, "y": 191.95, "z": 13.02}, {"x": 1503.55, "y": 193.55, "z": 13.0}, {"x": 1503.59, "y": 195.75, "z": 12.98}, {"x": 1503.53, "y": 197.95, "z": 12.95}, {"x": 1503.37, "y": 200.7, "z": 12.89}, {"x": 1503.05, "y": 204.2, "z": 12.82}, {"x": 1502.89, "y": 206.54, "z": 12.77}, {"x": 1502.67, "y": 208.89, "z": 12.73}, {"x": 1502.42, "y": 210.24, "z": 12.7}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1470.79, "y": 120.0, "z": 14.92}, {"x": 1471.2, "y": 120.82, "z": 14.86}, {"x": 1483.76, "y": 145.76, "z": 14.12}, {"x": 1497.47, "y": 173.06, "z": 13.44}, {"x": 1505.37, "y": 188.48, "z": 13.03}, {"x": 1507.38, "y": 192.34, "z": 12.98}, {"x": 1509.29, "y": 196.43, "z": 13.0}, {"x": 1509.93, "y": 198.27, "z": 13.0}, {"x": 1510.58, "y": 200.99, "z": 12.93}, {"x": 1510.58, "y": 201.01, "z": 12.93}, {"x": 1510.68, "y": 203.43, "z": 12.85}, {"x": 1510.34, "y": 205.87, "z": 12.81}, {"x": 1509.93, "y": 208.01, "z": 12.74}, {"x": 1509.27, "y": 210.41, "z": 12.72}, {"x": 1508.47, "y": 212.44, "z": 12.71}], "right_lane_mark_type": "NONE", "successors": [42806288, 42807644, 42806933, 42806677], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42811329}, "42812210": {"id": 42812210, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1604.2, "y": 224.73, "z": 13.36}, {"x": 1609.6, "y": 234.77, "z": 13.04}, {"x": 1612.96, "y": 240.91, "z": 12.86}, {"x": 1615.23, "y": 244.6, "z": 12.74}, {"x": 1617.38, "y": 248.02, "z": 12.63}, {"x": 1619.24, "y": 250.63, "z": 12.54}, {"x": 1622.21, "y": 254.58, "z": 12.41}, {"x": 1624.99, "y": 257.84, "z": 12.31}, {"x": 1629.07, "y": 262.2, "z": 12.12}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1607.54, "y": 223.3, "z": 13.42}, {"x": 1613.84, "y": 235.7, "z": 13.01}, {"x": 1616.08, "y": 239.71, "z": 12.87}, {"x": 1618.34, "y": 243.42, "z": 12.75}, {"x": 1621.1, "y": 247.43, "z": 12.6}, {"x": 1623.7, "y": 251.18, "z": 12.48}, {"x": 1625.99, "y": 254.27, "z": 12.37}, {"x": 1627.85, "y": 256.51, "z": 12.3}, {"x": 1631.2, "y": 260.05, "z": 12.11}], "right_lane_mark_type": "NONE", "successors": [42812503], "predecessors": [], "right_neighbor_id": 42812208, "left_neighbor_id": null}, "42812480": {"id": 42812480, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1605.35, "y": 258.22, "z": 12.45}, {"x": 1602.5, "y": 259.28, "z": 12.49}, {"x": 1600.01, "y": 260.07, "z": 12.52}, {"x": 1596.42, "y": 260.76, "z": 12.53}, {"x": 1594.13, "y": 260.96, "z": 12.54}, {"x": 1591.76, "y": 261.07, "z": 12.54}, {"x": 1590.0, "y": 261.18, "z": 12.54}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1607.02, "y": 262.27, "z": 12.38}, {"x": 1605.74, "y": 262.31, "z": 12.43}, {"x": 1604.25, "y": 262.46, "z": 12.46}, {"x": 1602.31, "y": 262.78, "z": 12.5}, {"x": 1600.88, "y": 263.08, "z": 12.49}, {"x": 1599.41, "y": 263.49, "z": 12.5}, {"x": 1598.03, "y": 263.93, "z": 12.51}, {"x": 1596.55, "y": 264.26, "z": 12.5}, {"x": 1595.08, "y": 264.43, "z": 12.53}, {"x": 1593.01, "y": 264.62, "z": 12.57}, {"x": 1591.67, "y": 264.68, "z": 12.55}, {"x": 1590.0, "y": 264.73, "z": 12.52}], "right_lane_mark_type": "NONE", "successors": [42811882], "predecessors": [42807549], "right_neighbor_id": null, "left_neighbor_id": 42811689}, "42812483": {"id": 42812483, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1578.81, "y": 251.78, "z": 12.53}, {"x": 1581.18, "y": 252.24, "z": 12.53}, {"x": 1582.28, "y": 252.36, "z": 12.54}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1579.93, "y": 248.81, "z": 12.5}, {"x": 1582.22, "y": 249.21, "z": 12.51}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42915544], "predecessors": [42811335], "right_neighbor_id": 42812494, "left_neighbor_id": 42807643}, "42812494": {"id": 42812494, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1579.93, "y": 248.81, "z": 12.5}, {"x": 1582.22, "y": 249.21, "z": 12.51}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1581.06, "y": 245.64, "z": 12.48}, {"x": 1582.72, "y": 245.79, "z": 12.49}], "right_lane_mark_type": "NONE", "successors": [42915650], "predecessors": [42811491], "right_neighbor_id": null, "left_neighbor_id": 42812483}, "42816214": {"id": 42816214, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1463.31, "y": 112.03, "z": 15.21}, {"x": 1467.39, "y": 120.0, "z": 14.99}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1466.09, "y": 110.75, "z": 15.14}, {"x": 1470.79, "y": 120.0, "z": 14.92}], "right_lane_mark_type": "NONE", "successors": [42811989], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42816397}, "42816397": {"id": 42816397, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1467.39, "y": 120.0, "z": 14.99}, {"x": 1463.31, "y": 112.03, "z": 15.21}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1460.72, "y": 120.0, "z": 15.09}, {"x": 1458.26, "y": 114.47, "z": 15.23}], "right_lane_mark_type": "NONE", "successors": [42824242, 42816401, 42824232, 42816934], "predecessors": [42811329], "right_neighbor_id": null, "left_neighbor_id": 42816214}, "42816401": {"id": 42816401, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1463.31, "y": 112.03, "z": 15.21}, {"x": 1445.72, "y": 83.34, "z": 16.0}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1458.26, "y": 114.47, "z": 15.23}, {"x": 1452.6, "y": 103.83, "z": 15.55}, {"x": 1451.58, "y": 101.81, "z": 15.6}, {"x": 1451.29, "y": 101.27, "z": 15.61}, {"x": 1451.18, "y": 101.09, "z": 15.61}, {"x": 1443.3, "y": 85.71, "z": 15.93}, {"x": 1443.29, "y": 85.7, "z": 15.93}, {"x": 1442.77, "y": 84.68, "z": 15.95}], "right_lane_mark_type": "NONE", "successors": [42816566], "predecessors": [42816397], "right_neighbor_id": null, "left_neighbor_id": null}, "42816624": {"id": 42816624, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1464.35, "y": 94.67, "z": 15.94}, {"x": 1462.8, "y": 95.55, "z": 15.86}, {"x": 1461.83, "y": 96.16, "z": 15.83}, {"x": 1460.91, "y": 96.82, "z": 15.77}, {"x": 1460.12, "y": 97.67, "z": 15.73}, {"x": 1459.6, "y": 98.39, "z": 15.7}, {"x": 1459.27, "y": 99.04, "z": 15.68}, {"x": 1459.02, "y": 99.77, "z": 15.64}, {"x": 1458.89, "y": 100.57, "z": 15.61}, {"x": 1458.91, "y": 101.49, "z": 15.56}, {"x": 1459.0, "y": 102.26, "z": 15.54}, {"x": 1459.16, "y": 103.17, "z": 15.5}, {"x": 1459.4, "y": 103.86, "z": 15.46}, {"x": 1459.8, "y": 104.73, "z": 15.43}, {"x": 1463.31, "y": 112.03, "z": 15.21}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1465.66, "y": 97.2, "z": 15.84}, {"x": 1465.22, "y": 97.41, "z": 15.82}, {"x": 1463.68, "y": 98.24, "z": 15.74}, {"x": 1463.18, "y": 98.53, "z": 15.69}, {"x": 1462.83, "y": 98.81, "z": 15.65}, {"x": 1462.69, "y": 98.92, "z": 15.65}, {"x": 1462.3, "y": 99.28, "z": 15.58}, {"x": 1462.01, "y": 99.69, "z": 15.52}, {"x": 1461.85, "y": 100.02, "z": 15.5}, {"x": 1461.72, "y": 100.47, "z": 15.5}, {"x": 1461.68, "y": 100.86, "z": 15.49}, {"x": 1461.68, "y": 101.34, "z": 15.48}, {"x": 1461.7, "y": 101.83, "z": 15.48}, {"x": 1461.75, "y": 102.02, "z": 15.49}, {"x": 1461.83, "y": 102.3, "z": 15.46}, {"x": 1461.97, "y": 102.67, "z": 15.43}, {"x": 1462.3, "y": 103.42, "z": 15.41}, {"x": 1466.09, "y": 110.75, "z": 15.14}], "right_lane_mark_type": "NONE", "successors": [42816214], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": null}, "42816877": {"id": 42816877, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1442.84, "y": 97.59, "z": 15.68}, {"x": 1445.22, "y": 96.51, "z": 15.73}, {"x": 1445.95, "y": 96.23, "z": 15.76}, {"x": 1446.58, "y": 96.06, "z": 15.76}, {"x": 1447.79, "y": 95.89, "z": 15.77}, {"x": 1448.79, "y": 95.89, "z": 15.78}, {"x": 1449.81, "y": 95.98, "z": 15.78}, {"x": 1450.84, "y": 96.22, "z": 15.78}, {"x": 1451.85, "y": 96.5, "z": 15.79}, {"x": 1452.56, "y": 96.85, "z": 15.79}, {"x": 1453.17, "y": 97.21, "z": 15.78}, {"x": 1453.86, "y": 97.65, "z": 15.77}, {"x": 1454.59, "y": 98.19, "z": 15.76}, {"x": 1455.55, "y": 99.08, "z": 15.73}, {"x": 1456.35, "y": 99.93, "z": 15.7}, {"x": 1457.3, "y": 101.08, "z": 15.64}, {"x": 1458.12, "y": 102.19, "z": 15.57}, {"x": 1458.85, "y": 103.4, "z": 15.49}, {"x": 1459.55, "y": 104.49, "z": 15.44}, {"x": 1463.31, "y": 112.03, "z": 15.21}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1441.28, "y": 94.27, "z": 15.76}, {"x": 1443.7, "y": 93.09, "z": 15.81}, {"x": 1444.95, "y": 92.64, "z": 15.83}, {"x": 1446.12, "y": 92.41, "z": 15.85}, {"x": 1447.3, "y": 92.27, "z": 15.86}, {"x": 1448.59, "y": 92.25, "z": 15.88}, {"x": 1449.97, "y": 92.39, "z": 15.88}, {"x": 1451.24, "y": 92.65, "z": 15.89}, {"x": 1452.41, "y": 93.08, "z": 15.89}, {"x": 1453.56, "y": 93.69, "z": 15.88}, {"x": 1454.92, "y": 94.65, "z": 15.86}, {"x": 1456.31, "y": 95.81, "z": 15.85}, {"x": 1457.34, "y": 96.84, "z": 15.82}, {"x": 1458.36, "y": 98.01, "z": 15.76}, {"x": 1460.36, "y": 100.69, "z": 15.53}, {"x": 1466.09, "y": 110.75, "z": 15.14}], "right_lane_mark_type": "NONE", "successors": [42816214], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42816934}, "42816934": {"id": 42816934, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1463.31, "y": 112.03, "z": 15.21}, {"x": 1459.55, "y": 104.49, "z": 15.44}, {"x": 1458.85, "y": 103.4, "z": 15.49}, {"x": 1458.12, "y": 102.19, "z": 15.57}, {"x": 1457.3, "y": 101.08, "z": 15.64}, {"x": 1456.35, "y": 99.93, "z": 15.7}, {"x": 1455.55, "y": 99.08, "z": 15.73}, {"x": 1454.59, "y": 98.19, "z": 15.76}, {"x": 1453.86, "y": 97.65, "z": 15.77}, {"x": 1453.17, "y": 97.21, "z": 15.78}, {"x": 1452.56, "y": 96.85, "z": 15.79}, {"x": 1451.85, "y": 96.5, "z": 15.79}, {"x": 1450.84, "y": 96.22, "z": 15.78}, {"x": 1449.81, "y": 95.98, "z": 15.78}, {"x": 1448.79, "y": 95.89, "z": 15.78}, {"x": 1447.79, "y": 95.89, "z": 15.77}, {"x": 1446.58, "y": 96.06, "z": 15.76}, {"x": 1445.95, "y": 96.23, "z": 15.76}, {"x": 1445.22, "y": 96.51, "z": 15.73}, {"x": 1442.84, "y": 97.59, "z": 15.68}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1458.26, "y": 114.47, "z": 15.23}, {"x": 1453.78, "y": 105.96, "z": 15.47}, {"x": 1451.58, "y": 101.81, "z": 15.6}, {"x": 1451.29, "y": 101.27, "z": 15.61}, {"x": 1451.11, "y": 100.97, "z": 15.61}, {"x": 1450.92, "y": 100.72, "z": 15.61}, {"x": 1450.69, "y": 100.55, "z": 15.62}, {"x": 1450.34, "y": 100.33, "z": 15.62}, {"x": 1449.91, "y": 100.14, "z": 15.62}, {"x": 1449.52, "y": 99.96, "z": 15.62}, {"x": 1448.96, "y": 99.86, "z": 15.61}, {"x": 1448.55, "y": 99.86, "z": 15.62}, {"x": 1448.05, "y": 99.87, "z": 15.62}, {"x": 1447.4, "y": 99.99, "z": 15.62}, {"x": 1446.82, "y": 100.12, "z": 15.62}, {"x": 1446.02, "y": 100.29, "z": 15.61}, {"x": 1445.38, "y": 100.48, "z": 15.62}, {"x": 1444.69, "y": 100.66, "z": 15.61}], "right_lane_mark_type": "NONE", "successors": [42816620], "predecessors": [42816397], "right_neighbor_id": null, "left_neighbor_id": 42816877}, "42817773": {"id": 42817773, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1454.58, "y": 306.44, "z": 11.49}, {"x": 1455.36, "y": 306.65, "z": 11.49}, {"x": 1456.2, "y": 306.84, "z": 11.48}, {"x": 1457.1, "y": 307.0, "z": 11.47}, {"x": 1458.04, "y": 307.12, "z": 11.45}, {"x": 1459.02, "y": 307.2, "z": 11.44}, {"x": 1460.02, "y": 307.24, "z": 11.43}, {"x": 1461.03, "y": 307.23, "z": 11.42}, {"x": 1462.04, "y": 307.16, "z": 11.41}, {"x": 1463.04, "y": 307.03, "z": 11.41}, {"x": 1464.01, "y": 306.83, "z": 11.41}, {"x": 1464.94, "y": 306.56, "z": 11.4}, {"x": 1465.82, "y": 306.22, "z": 11.4}, {"x": 1466.64, "y": 305.79, "z": 11.4}, {"x": 1467.39, "y": 305.28, "z": 11.41}, {"x": 1468.05, "y": 304.67, "z": 11.42}, {"x": 1468.62, "y": 303.97, "z": 11.43}, {"x": 1469.08, "y": 303.16, "z": 11.44}, {"x": 1469.41, "y": 302.25, "z": 11.44}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1455.03, "y": 305.07, "z": 11.51}, {"x": 1455.55, "y": 305.31, "z": 11.52}, {"x": 1456.14, "y": 305.53, "z": 11.52}, {"x": 1456.77, "y": 305.72, "z": 11.5}, {"x": 1457.44, "y": 305.84, "z": 11.47}, {"x": 1458.14, "y": 305.87, "z": 11.45}, {"x": 1458.84, "y": 305.8, "z": 11.43}, {"x": 1459.54, "y": 305.59, "z": 11.41}, {"x": 1460.23, "y": 305.22, "z": 11.4}, {"x": 1460.65, "y": 304.61, "z": 11.42}, {"x": 1460.94, "y": 304.29, "z": 11.42}, {"x": 1461.12, "y": 303.91, "z": 11.42}, {"x": 1461.65, "y": 302.77, "z": 11.47}, {"x": 1461.94, "y": 302.16, "z": 11.47}, {"x": 1462.35, "y": 301.29, "z": 11.49}, {"x": 1462.68, "y": 300.59, "z": 11.46}, {"x": 1462.92, "y": 300.07, "z": 11.41}], "right_lane_mark_type": "NONE", "successors": [42809705], "predecessors": [42810823], "right_neighbor_id": null, "left_neighbor_id": 42817783}, "42817778": {"id": 42817778, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1481.38, "y": 316.09, "z": 11.26}, {"x": 1480.81, "y": 315.84, "z": 11.26}, {"x": 1480.15, "y": 315.53, "z": 11.27}, {"x": 1479.43, "y": 315.18, "z": 11.27}, {"x": 1478.65, "y": 314.78, "z": 11.27}, {"x": 1477.84, "y": 314.33, "z": 11.27}, {"x": 1476.99, "y": 313.84, "z": 11.28}, {"x": 1476.12, "y": 313.31, "z": 11.29}, {"x": 1475.26, "y": 312.74, "z": 11.3}, {"x": 1474.39, "y": 312.14, "z": 11.3}, {"x": 1473.55, "y": 311.51, "z": 11.31}, {"x": 1472.74, "y": 310.84, "z": 11.32}, {"x": 1471.97, "y": 310.15, "z": 11.33}, {"x": 1471.26, "y": 309.44, "z": 11.34}, {"x": 1470.62, "y": 308.7, "z": 11.35}, {"x": 1470.05, "y": 307.94, "z": 11.36}, {"x": 1469.58, "y": 307.16, "z": 11.37}, {"x": 1469.22, "y": 306.37, "z": 11.38}, {"x": 1468.97, "y": 305.56, "z": 11.39}, {"x": 1468.86, "y": 304.74, "z": 11.41}, {"x": 1468.88, "y": 303.92, "z": 11.43}, {"x": 1469.07, "y": 303.08, "z": 11.44}, {"x": 1469.41, "y": 302.25, "z": 11.44}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1480.93, "y": 317.3, "z": 11.27}, {"x": 1477.36, "y": 316.06, "z": 11.29}, {"x": 1477.16, "y": 316.11, "z": 11.29}, {"x": 1476.65, "y": 315.93, "z": 11.29}, {"x": 1475.75, "y": 315.58, "z": 11.29}, {"x": 1474.85, "y": 315.21, "z": 11.29}, {"x": 1473.96, "y": 314.81, "z": 11.3}, {"x": 1473.07, "y": 314.39, "z": 11.31}, {"x": 1472.18, "y": 313.95, "z": 11.33}, {"x": 1471.32, "y": 313.48, "z": 11.34}, {"x": 1470.47, "y": 312.98, "z": 11.34}, {"x": 1469.64, "y": 312.46, "z": 11.35}, {"x": 1468.84, "y": 311.91, "z": 11.37}, {"x": 1468.07, "y": 311.34, "z": 11.37}, {"x": 1467.34, "y": 310.73, "z": 11.38}, {"x": 1466.65, "y": 310.1, "z": 11.39}, {"x": 1466.0, "y": 309.44, "z": 11.39}, {"x": 1465.39, "y": 308.74, "z": 11.39}, {"x": 1464.84, "y": 308.02, "z": 11.39}, {"x": 1464.35, "y": 307.27, "z": 11.4}, {"x": 1463.91, "y": 306.48, "z": 11.41}, {"x": 1463.54, "y": 305.67, "z": 11.41}, {"x": 1463.24, "y": 304.82, "z": 11.4}, {"x": 1463.02, "y": 303.94, "z": 11.4}, {"x": 1462.87, "y": 303.02, "z": 11.39}, {"x": 1462.8, "y": 302.08, "z": 11.4}, {"x": 1462.81, "y": 301.09, "z": 11.41}, {"x": 1462.92, "y": 300.07, "z": 11.41}], "right_lane_mark_type": "NONE", "successors": [42809705], "predecessors": [42808753], "right_neighbor_id": null, "left_neighbor_id": null}, "42817783": {"id": 42817783, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1469.41, "y": 302.25, "z": 11.44}, {"x": 1469.08, "y": 303.16, "z": 11.44}, {"x": 1468.62, "y": 303.97, "z": 11.43}, {"x": 1468.05, "y": 304.67, "z": 11.42}, {"x": 1467.39, "y": 305.28, "z": 11.41}, {"x": 1466.64, "y": 305.79, "z": 11.4}, {"x": 1465.82, "y": 306.22, "z": 11.4}, {"x": 1464.94, "y": 306.56, "z": 11.4}, {"x": 1464.01, "y": 306.83, "z": 11.41}, {"x": 1463.04, "y": 307.03, "z": 11.41}, {"x": 1462.04, "y": 307.16, "z": 11.41}, {"x": 1461.03, "y": 307.23, "z": 11.42}, {"x": 1460.02, "y": 307.24, "z": 11.43}, {"x": 1459.02, "y": 307.2, "z": 11.44}, {"x": 1458.04, "y": 307.12, "z": 11.45}, {"x": 1457.1, "y": 307.0, "z": 11.47}, {"x": 1456.2, "y": 306.84, "z": 11.48}, {"x": 1455.36, "y": 306.65, "z": 11.49}, {"x": 1454.58, "y": 306.44, "z": 11.49}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1472.35, "y": 303.23, "z": 11.4}, {"x": 1471.91, "y": 304.13, "z": 11.39}, {"x": 1471.4, "y": 304.95, "z": 11.38}, {"x": 1470.82, "y": 305.69, "z": 11.38}, {"x": 1470.17, "y": 306.35, "z": 11.37}, {"x": 1469.47, "y": 306.93, "z": 11.37}, {"x": 1468.7, "y": 307.44, "z": 11.38}, {"x": 1467.89, "y": 307.87, "z": 11.38}, {"x": 1467.04, "y": 308.24, "z": 11.38}, {"x": 1466.15, "y": 308.55, "z": 11.39}, {"x": 1465.23, "y": 308.79, "z": 11.4}, {"x": 1464.29, "y": 308.97, "z": 11.41}, {"x": 1463.33, "y": 309.1, "z": 11.42}, {"x": 1462.35, "y": 309.17, "z": 11.43}, {"x": 1461.37, "y": 309.19, "z": 11.44}, {"x": 1460.39, "y": 309.17, "z": 11.45}, {"x": 1459.41, "y": 309.09, "z": 11.46}, {"x": 1458.45, "y": 308.98, "z": 11.47}, {"x": 1457.5, "y": 308.82, "z": 11.48}, {"x": 1456.57, "y": 308.63, "z": 11.49}, {"x": 1455.67, "y": 308.4, "z": 11.49}, {"x": 1454.81, "y": 308.15, "z": 11.5}, {"x": 1453.98, "y": 307.86, "z": 11.51}], "right_lane_mark_type": "NONE", "successors": [42808987], "predecessors": [42808644], "right_neighbor_id": null, "left_neighbor_id": 42817773}, "42817801": {"id": 42817801, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1459.23, "y": 329.93, "z": 10.98}, {"x": 1459.54, "y": 329.0, "z": 11.01}, {"x": 1459.85, "y": 328.16, "z": 11.02}, {"x": 1460.25, "y": 327.33, "z": 11.04}, {"x": 1460.7, "y": 326.52, "z": 11.06}, {"x": 1461.19, "y": 325.72, "z": 11.09}, {"x": 1461.72, "y": 324.93, "z": 11.11}, {"x": 1462.28, "y": 324.16, "z": 11.14}, {"x": 1462.88, "y": 323.41, "z": 11.16}, {"x": 1463.51, "y": 322.67, "z": 11.18}, {"x": 1464.17, "y": 321.97, "z": 11.21}, {"x": 1464.87, "y": 321.28, "z": 11.23}, {"x": 1465.59, "y": 320.63, "z": 11.25}, {"x": 1466.33, "y": 320.0, "z": 11.26}, {"x": 1467.11, "y": 319.41, "z": 11.27}, {"x": 1467.9, "y": 318.85, "z": 11.29}, {"x": 1468.72, "y": 318.32, "z": 11.3}, {"x": 1469.55, "y": 317.84, "z": 11.31}, {"x": 1470.4, "y": 317.39, "z": 11.32}, {"x": 1471.27, "y": 316.99, "z": 11.32}, {"x": 1472.15, "y": 316.64, "z": 11.31}, {"x": 1473.05, "y": 316.33, "z": 11.31}, {"x": 1473.95, "y": 316.08, "z": 11.3}, {"x": 1474.87, "y": 315.87, "z": 11.29}, {"x": 1475.79, "y": 315.72, "z": 11.29}, {"x": 1476.72, "y": 315.63, "z": 11.29}, {"x": 1477.65, "y": 315.6, "z": 11.28}, {"x": 1478.58, "y": 315.62, "z": 11.27}, {"x": 1479.52, "y": 315.71, "z": 11.27}, {"x": 1480.45, "y": 315.87, "z": 11.26}, {"x": 1481.38, "y": 316.09, "z": 11.26}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1456.07, "y": 328.82, "z": 10.93}, {"x": 1456.42, "y": 328.0, "z": 10.95}, {"x": 1456.81, "y": 327.19, "z": 10.97}, {"x": 1457.23, "y": 326.38, "z": 11.0}, {"x": 1457.7, "y": 325.58, "z": 11.03}, {"x": 1458.2, "y": 324.79, "z": 11.06}, {"x": 1458.74, "y": 324.01, "z": 11.09}, {"x": 1459.31, "y": 323.25, "z": 11.12}, {"x": 1459.91, "y": 322.5, "z": 11.15}, {"x": 1460.54, "y": 321.77, "z": 11.18}, {"x": 1461.2, "y": 321.06, "z": 11.21}, {"x": 1461.89, "y": 320.37, "z": 11.24}, {"x": 1462.61, "y": 319.7, "z": 11.26}, {"x": 1463.35, "y": 319.06, "z": 11.28}, {"x": 1464.11, "y": 318.45, "z": 11.3}, {"x": 1464.9, "y": 317.86, "z": 11.31}, {"x": 1465.7, "y": 317.31, "z": 11.32}, {"x": 1466.53, "y": 316.79, "z": 11.33}, {"x": 1467.37, "y": 316.3, "z": 11.34}, {"x": 1468.23, "y": 315.85, "z": 11.35}, {"x": 1469.1, "y": 315.44, "z": 11.34}, {"x": 1469.98, "y": 315.06, "z": 11.34}, {"x": 1470.88, "y": 314.73, "z": 11.34}, {"x": 1471.78, "y": 314.44, "z": 11.33}, {"x": 1472.7, "y": 314.2, "z": 11.32}, {"x": 1473.62, "y": 314.0, "z": 11.31}, {"x": 1474.54, "y": 313.85, "z": 11.3}, {"x": 1475.47, "y": 313.75, "z": 11.29}, {"x": 1476.41, "y": 313.71, "z": 11.28}, {"x": 1477.34, "y": 313.71, "z": 11.28}, {"x": 1478.27, "y": 313.78, "z": 11.29}, {"x": 1479.2, "y": 313.9, "z": 11.27}, {"x": 1480.13, "y": 314.08, "z": 11.29}, {"x": 1481.05, "y": 314.33, "z": 11.32}, {"x": 1481.97, "y": 314.63, "z": 11.28}], "right_lane_mark_type": "NONE", "successors": [42808752], "predecessors": [42808548], "right_neighbor_id": null, "left_neighbor_id": null}, "42817814": {"id": 42817814, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1472.35, "y": 303.23, "z": 11.4}, {"x": 1471.88, "y": 304.26, "z": 11.38}, {"x": 1471.54, "y": 305.26, "z": 11.37}, {"x": 1471.34, "y": 306.23, "z": 11.36}, {"x": 1471.26, "y": 307.17, "z": 11.35}, {"x": 1471.3, "y": 308.07, "z": 11.35}, {"x": 1471.45, "y": 308.95, "z": 11.34}, {"x": 1471.7, "y": 309.78, "z": 11.33}, {"x": 1472.05, "y": 310.57, "z": 11.33}, {"x": 1472.49, "y": 311.32, "z": 11.33}, {"x": 1473.01, "y": 312.03, "z": 11.32}, {"x": 1473.61, "y": 312.68, "z": 11.31}, {"x": 1474.28, "y": 313.29, "z": 11.3}, {"x": 1475.07, "y": 313.77, "z": 11.3}, {"x": 1475.78, "y": 314.01, "z": 11.29}, {"x": 1476.6, "y": 314.3, "z": 11.28}, {"x": 1477.34, "y": 314.56, "z": 11.28}, {"x": 1478.27, "y": 315.0, "z": 11.27}, {"x": 1479.15, "y": 315.21, "z": 11.27}, {"x": 1480.34, "y": 315.67, "z": 11.26}, {"x": 1481.38, "y": 316.09, "z": 11.26}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1475.18, "y": 304.11, "z": 11.35}, {"x": 1474.73, "y": 305.15, "z": 11.37}, {"x": 1474.41, "y": 306.14, "z": 11.38}, {"x": 1474.21, "y": 307.06, "z": 11.35}, {"x": 1474.14, "y": 307.93, "z": 11.34}, {"x": 1474.21, "y": 308.85, "z": 11.33}, {"x": 1474.21, "y": 308.94, "z": 11.32}, {"x": 1474.35, "y": 309.87, "z": 11.32}, {"x": 1474.77, "y": 310.85, "z": 11.31}, {"x": 1475.59, "y": 311.82, "z": 11.32}, {"x": 1476.74, "y": 312.57, "z": 11.33}, {"x": 1477.64, "y": 312.98, "z": 11.33}, {"x": 1480.64, "y": 314.06, "z": 11.32}, {"x": 1481.97, "y": 314.63, "z": 11.28}], "right_lane_mark_type": "NONE", "successors": [42808752], "predecessors": [42808641], "right_neighbor_id": null, "left_neighbor_id": null}, "42817815": {"id": 42817815, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1456.07, "y": 328.82, "z": 10.93}, {"x": 1456.27, "y": 328.23, "z": 10.95}, {"x": 1456.5, "y": 327.55, "z": 10.96}, {"x": 1456.74, "y": 326.79, "z": 10.98}, {"x": 1457.0, "y": 325.96, "z": 11.0}, {"x": 1457.27, "y": 325.07, "z": 11.03}, {"x": 1457.54, "y": 324.13, "z": 11.06}, {"x": 1457.81, "y": 323.14, "z": 11.1}, {"x": 1458.07, "y": 322.11, "z": 11.12}, {"x": 1458.31, "y": 321.06, "z": 11.17}, {"x": 1458.54, "y": 319.98, "z": 11.22}, {"x": 1458.73, "y": 318.89, "z": 11.26}, {"x": 1458.9, "y": 317.79, "z": 11.29}, {"x": 1459.03, "y": 316.7, "z": 11.32}, {"x": 1459.11, "y": 315.62, "z": 11.36}, {"x": 1459.14, "y": 314.55, "z": 11.39}, {"x": 1459.12, "y": 313.52, "z": 11.41}, {"x": 1459.04, "y": 312.52, "z": 11.43}, {"x": 1458.89, "y": 311.56, "z": 11.45}, {"x": 1458.66, "y": 310.66, "z": 11.46}, {"x": 1458.36, "y": 309.81, "z": 11.47}, {"x": 1457.98, "y": 309.04, "z": 11.47}, {"x": 1457.5, "y": 308.34, "z": 11.47}, {"x": 1456.93, "y": 307.72, "z": 11.48}, {"x": 1456.26, "y": 307.19, "z": 11.48}, {"x": 1455.48, "y": 306.76, "z": 11.49}, {"x": 1454.58, "y": 306.44, "z": 11.49}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1452.8, "y": 327.68, "z": 10.92}, {"x": 1453.0, "y": 327.17, "z": 10.92}, {"x": 1453.24, "y": 326.55, "z": 10.96}, {"x": 1453.51, "y": 325.84, "z": 10.99}, {"x": 1453.82, "y": 325.05, "z": 10.99}, {"x": 1454.14, "y": 324.18, "z": 11.04}, {"x": 1454.47, "y": 323.25, "z": 11.08}, {"x": 1454.81, "y": 322.26, "z": 11.1}, {"x": 1455.14, "y": 321.24, "z": 11.14}, {"x": 1455.45, "y": 320.18, "z": 11.2}, {"x": 1455.75, "y": 319.1, "z": 11.21}, {"x": 1456.01, "y": 318.01, "z": 11.24}, {"x": 1456.24, "y": 316.92, "z": 11.28}, {"x": 1456.42, "y": 315.84, "z": 11.33}, {"x": 1456.55, "y": 314.78, "z": 11.36}, {"x": 1456.62, "y": 313.75, "z": 11.4}, {"x": 1456.62, "y": 312.76, "z": 11.42}, {"x": 1456.54, "y": 311.82, "z": 11.45}, {"x": 1456.37, "y": 310.95, "z": 11.46}, {"x": 1456.11, "y": 310.14, "z": 11.47}, {"x": 1455.75, "y": 309.42, "z": 11.48}, {"x": 1455.28, "y": 308.8, "z": 11.49}, {"x": 1454.7, "y": 308.27, "z": 11.5}, {"x": 1453.98, "y": 307.86, "z": 11.51}], "right_lane_mark_type": "NONE", "successors": [42808987], "predecessors": [42809680], "right_neighbor_id": null, "left_neighbor_id": null}, "42817816": {"id": 42817816, "is_intersection": true, "lane_type": "BIKE", "left_lane_boundary": [{"x": 1454.58, "y": 306.44, "z": 11.49}, {"x": 1455.45, "y": 306.91, "z": 11.49}, {"x": 1456.25, "y": 307.43, "z": 11.49}, {"x": 1456.98, "y": 308.0, "z": 11.48}, {"x": 1457.64, "y": 308.63, "z": 11.47}, {"x": 1458.24, "y": 309.3, "z": 11.47}, {"x": 1458.77, "y": 310.02, "z": 11.46}, {"x": 1459.25, "y": 310.79, "z": 11.46}, {"x": 1459.67, "y": 311.59, "z": 11.44}, {"x": 1460.03, "y": 312.42, "z": 11.43}, {"x": 1460.34, "y": 313.28, "z": 11.41}, {"x": 1460.59, "y": 314.17, "z": 11.4}, {"x": 1460.8, "y": 315.08, "z": 11.38}, {"x": 1460.96, "y": 316.01, "z": 11.35}, {"x": 1461.08, "y": 316.96, "z": 11.32}, {"x": 1461.15, "y": 317.92, "z": 11.3}, {"x": 1461.18, "y": 318.89, "z": 11.27}, {"x": 1461.17, "y": 319.86, "z": 11.24}, {"x": 1461.13, "y": 320.83, "z": 11.21}, {"x": 1461.05, "y": 321.8, "z": 11.19}, {"x": 1460.94, "y": 322.77, "z": 11.16}, {"x": 1460.8, "y": 323.72, "z": 11.13}, {"x": 1460.63, "y": 324.66, "z": 11.11}, {"x": 1460.44, "y": 325.59, "z": 11.08}, {"x": 1460.22, "y": 326.49, "z": 11.06}, {"x": 1459.99, "y": 327.37, "z": 11.04}, {"x": 1459.73, "y": 328.22, "z": 11.02}, {"x": 1459.47, "y": 329.18, "z": 11.0}, {"x": 1459.23, "y": 329.93, "z": 10.98}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1455.03, "y": 305.07, "z": 11.51}, {"x": 1455.98, "y": 305.44, "z": 11.54}, {"x": 1456.87, "y": 305.88, "z": 11.47}, {"x": 1457.7, "y": 306.37, "z": 11.45}, {"x": 1458.47, "y": 306.92, "z": 11.44}, {"x": 1459.18, "y": 307.53, "z": 11.44}, {"x": 1459.83, "y": 308.18, "z": 11.44}, {"x": 1460.43, "y": 308.88, "z": 11.44}, {"x": 1460.97, "y": 309.62, "z": 11.44}, {"x": 1461.46, "y": 310.4, "z": 11.44}, {"x": 1461.91, "y": 311.21, "z": 11.44}, {"x": 1462.3, "y": 312.06, "z": 11.42}, {"x": 1462.64, "y": 312.94, "z": 11.4}, {"x": 1462.94, "y": 313.85, "z": 11.39}, {"x": 1463.19, "y": 314.78, "z": 11.38}, {"x": 1463.4, "y": 315.73, "z": 11.36}, {"x": 1463.57, "y": 316.69, "z": 11.33}, {"x": 1463.7, "y": 317.67, "z": 11.31}, {"x": 1463.79, "y": 318.66, "z": 11.29}, {"x": 1463.85, "y": 319.65, "z": 11.27}, {"x": 1463.87, "y": 320.65, "z": 11.24}, {"x": 1463.86, "y": 321.65, "z": 11.22}, {"x": 1463.81, "y": 322.65, "z": 11.19}, {"x": 1463.74, "y": 323.64, "z": 11.16}, {"x": 1463.64, "y": 324.62, "z": 11.13}, {"x": 1463.51, "y": 325.58, "z": 11.1}, {"x": 1463.35, "y": 326.53, "z": 11.08}, {"x": 1463.18, "y": 327.46, "z": 11.05}, {"x": 1462.98, "y": 328.37, "z": 11.03}, {"x": 1462.76, "y": 329.25, "z": 11.0}, {"x": 1462.52, "y": 330.1, "z": 10.98}, {"x": 1462.26, "y": 330.92, "z": 10.96}], "right_lane_mark_type": "NONE", "successors": [42817689], "predecessors": [42810823], "right_neighbor_id": null, "left_neighbor_id": null}, "42817998": {"id": 42817998, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1584.5, "y": 177.61, "z": 14.92}, {"x": 1584.79, "y": 178.19, "z": 14.91}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1587.01, "y": 175.43, "z": 14.86}, {"x": 1587.25, "y": 175.92, "z": 14.84}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42818516], "predecessors": [], "right_neighbor_id": 42818035, "left_neighbor_id": null}, "42817999": {"id": 42817999, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1555.95, "y": 120.0, "z": 16.99}, {"x": 1575.02, "y": 158.51, "z": 15.61}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1559.45, "y": 120.0, "z": 16.86}, {"x": 1577.95, "y": 157.04, "z": 15.51}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42818481], "predecessors": [42816935], "right_neighbor_id": 42818513, "left_neighbor_id": 42818036}, "42818012": {"id": 42818012, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1578.57, "y": 180.58, "z": 14.84}, {"x": 1569.08, "y": 161.48, "z": 15.52}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1575.4, "y": 182.02, "z": 14.88}, {"x": 1565.96, "y": 163.03, "z": 15.47}], "right_lane_mark_type": "NONE", "successors": [42818549], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42818546}, "42818021": {"id": 42818021, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1577.95, "y": 157.04, "z": 15.51}, {"x": 1587.01, "y": 175.43, "z": 14.86}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1581.04, "y": 155.48, "z": 15.52}, {"x": 1588.28, "y": 170.66, "z": 14.95}, {"x": 1589.24, "y": 172.77, "z": 14.87}], "right_lane_mark_type": "NONE", "successors": [42818035], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42818481}, "42818022": {"id": 42818022, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1587.34, "y": 190.94, "z": 14.49}, {"x": 1581.99, "y": 179.85, "z": 14.91}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1584.48, "y": 192.36, "z": 14.41}, {"x": 1579.02, "y": 181.48, "z": 14.8}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42818454], "predecessors": [], "right_neighbor_id": 42818040, "left_neighbor_id": null}, "42818025": {"id": 42818025, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1579.02, "y": 181.48, "z": 14.8}, {"x": 1578.57, "y": 180.58, "z": 14.84}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1575.97, "y": 183.16, "z": 14.75}, {"x": 1575.4, "y": 182.02, "z": 14.88}], "right_lane_mark_type": "NONE", "successors": [42818012], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42818454}, "42818026": {"id": 42818026, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1571.88, "y": 160.06, "z": 15.59}, {"x": 1552.04, "y": 120.0, "z": 17.07}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1569.08, "y": 161.48, "z": 15.52}, {"x": 1548.7, "y": 120.0, "z": 17.07}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42816385], "predecessors": [], "right_neighbor_id": 42818549, "left_neighbor_id": 42818036}, "42818027": {"id": 42818027, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1590.0, "y": 203.32, "z": 14.06}, {"x": 1584.48, "y": 192.36, "z": 14.41}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1590.0, "y": 211.38, "z": 13.85}, {"x": 1581.32, "y": 193.93, "z": 14.44}], "right_lane_mark_type": "NONE", "successors": [42818040], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42818528}, "42818035": {"id": 42818035, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1587.01, "y": 175.43, "z": 14.86}, {"x": 1587.25, "y": 175.92, "z": 14.84}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1589.24, "y": 172.77, "z": 14.87}, {"x": 1589.47, "y": 173.87, "z": 14.81}], "right_lane_mark_type": "NONE", "successors": [42818485], "predecessors": [42818021], "right_neighbor_id": null, "left_neighbor_id": 42817998}, "42818036": {"id": 42818036, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1575.02, "y": 158.51, "z": 15.61}, {"x": 1555.95, "y": 120.0, "z": 16.99}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1571.88, "y": 160.06, "z": 15.59}, {"x": 1552.04, "y": 120.0, "z": 17.07}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42815726], "predecessors": [], "right_neighbor_id": 42818026, "left_neighbor_id": 42817999}, "42818040": {"id": 42818040, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1584.48, "y": 192.36, "z": 14.41}, {"x": 1579.02, "y": 181.48, "z": 14.8}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1581.32, "y": 193.93, "z": 14.44}, {"x": 1575.97, "y": 183.16, "z": 14.75}], "right_lane_mark_type": "NONE", "successors": [42818025], "predecessors": [42818027], "right_neighbor_id": null, "left_neighbor_id": 42818022}, "42818454": {"id": 42818454, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1581.99, "y": 179.85, "z": 14.91}, {"x": 1581.6, "y": 179.06, "z": 14.93}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1579.02, "y": 181.48, "z": 14.8}, {"x": 1578.57, "y": 180.58, "z": 14.84}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42818546], "predecessors": [42818022], "right_neighbor_id": 42818025, "left_neighbor_id": null}, "42818465": {"id": 42818465, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1584.5, "y": 177.61, "z": 14.92}, {"x": 1575.02, "y": 158.51, "z": 15.61}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1581.6, "y": 179.06, "z": 14.93}, {"x": 1571.88, "y": 160.06, "z": 15.59}], "right_lane_mark_type": "NONE", "successors": [42818036], "predecessors": [], "right_neighbor_id": 42818546, "left_neighbor_id": 42818481}, "42818481": {"id": 42818481, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1575.02, "y": 158.51, "z": 15.61}, {"x": 1584.5, "y": 177.61, "z": 14.92}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1577.95, "y": 157.04, "z": 15.51}, {"x": 1587.01, "y": 175.43, "z": 14.86}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42817998], "predecessors": [42817999], "right_neighbor_id": 42818021, "left_neighbor_id": 42818465}, "42818485": {"id": 42818485, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1587.25, "y": 175.92, "z": 14.84}, {"x": 1590.0, "y": 181.11, "z": 14.65}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1589.47, "y": 173.87, "z": 14.81}, {"x": 1590.0, "y": 174.37, "z": 14.8}], "right_lane_mark_type": "NONE", "successors": [42819421], "predecessors": [42818035], "right_neighbor_id": null, "left_neighbor_id": 42818516}, "42818513": {"id": 42818513, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1559.45, "y": 120.0, "z": 16.86}, {"x": 1577.95, "y": 157.04, "z": 15.51}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1563.47, "y": 120.0, "z": 16.81}, {"x": 1580.19, "y": 153.78, "z": 15.57}, {"x": 1581.04, "y": 155.48, "z": 15.52}], "right_lane_mark_type": "NONE", "successors": [42818021], "predecessors": [42815727], "right_neighbor_id": null, "left_neighbor_id": 42817999}, "42818516": {"id": 42818516, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1584.79, "y": 178.19, "z": 14.91}, {"x": 1590.0, "y": 188.91, "z": 14.52}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1587.25, "y": 175.92, "z": 14.84}, {"x": 1590.0, "y": 181.11, "z": 14.65}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42819408], "predecessors": [42817998], "right_neighbor_id": 42818485, "left_neighbor_id": null}, "42818528": {"id": 42818528, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1590.0, "y": 196.47, "z": 14.31}, {"x": 1587.34, "y": 190.94, "z": 14.49}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1590.0, "y": 203.32, "z": 14.06}, {"x": 1584.48, "y": 192.36, "z": 14.41}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42818022], "predecessors": [], "right_neighbor_id": 42818027, "left_neighbor_id": null}, "42818546": {"id": 42818546, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1581.6, "y": 179.06, "z": 14.93}, {"x": 1571.88, "y": 160.06, "z": 15.59}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1578.57, "y": 180.58, "z": 14.84}, {"x": 1569.08, "y": 161.48, "z": 15.52}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42818026], "predecessors": [42818454], "right_neighbor_id": 42818012, "left_neighbor_id": 42818465}, "42818549": {"id": 42818549, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1569.08, "y": 161.48, "z": 15.52}, {"x": 1548.7, "y": 120.0, "z": 17.07}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1565.96, "y": 163.03, "z": 15.47}, {"x": 1544.62, "y": 120.0, "z": 17.14}], "right_lane_mark_type": "NONE", "successors": [42816384], "predecessors": [42818012], "right_neighbor_id": null, "left_neighbor_id": 42818026}, "42818858": {"id": 42818858, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1593.93, "y": 204.21, "z": 14.02}, {"x": 1604.2, "y": 224.73, "z": 13.36}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1597.04, "y": 202.52, "z": 14.01}, {"x": 1607.54, "y": 223.3, "z": 13.42}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42812210], "predecessors": [], "right_neighbor_id": 42819649, "left_neighbor_id": 42819224}, "42818861": {"id": 42818861, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1593.93, "y": 204.21, "z": 14.02}, {"x": 1590.0, "y": 196.47, "z": 14.31}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1591.14, "y": 205.77, "z": 13.97}, {"x": 1590.0, "y": 203.32, "z": 14.06}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42818528], "predecessors": [], "right_neighbor_id": 42820396, "left_neighbor_id": null}, "42819006": {"id": 42819006, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1590.73, "y": 190.34, "z": 14.47}, {"x": 1597.04, "y": 202.52, "z": 14.01}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1593.84, "y": 188.76, "z": 14.37}, {"x": 1599.88, "y": 201.47, "z": 13.96}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42819649], "predecessors": [], "right_neighbor_id": 42819650, "left_neighbor_id": null}, "42819070": {"id": 42819070, "is_intersection": false, "lane_type": "BUS", "left_lane_boundary": [{"x": 1377.14, "y": 150.0, "z": 14.0}, {"x": 1382.0, "y": 159.55, "z": 13.74}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1380.87, "y": 150.0, "z": 13.95}, {"x": 1383.11, "y": 154.42, "z": 13.82}, {"x": 1384.98, "y": 158.11, "z": 13.75}], "right_lane_mark_type": "NONE", "successors": [42811887], "predecessors": [42819028], "right_neighbor_id": null, "left_neighbor_id": 42819068}, "42819224": {"id": 42819224, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1604.2, "y": 224.73, "z": 13.36}, {"x": 1593.93, "y": 204.21, "z": 14.02}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1601.14, "y": 226.04, "z": 13.24}, {"x": 1591.14, "y": 205.77, "z": 13.97}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42818861], "predecessors": [42807779, 42809415], "right_neighbor_id": 42819444, "left_neighbor_id": 42818858}, "42819408": {"id": 42819408, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1590.0, "y": 188.91, "z": 14.52}, {"x": 1590.73, "y": 190.34, "z": 14.47}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1590.0, "y": 181.11, "z": 14.65}, {"x": 1593.84, "y": 188.76, "z": 14.37}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42819006, 42819427], "predecessors": [42818516], "right_neighbor_id": 42819421, "left_neighbor_id": null}, "42819421": {"id": 42819421, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1590.0, "y": 181.11, "z": 14.65}, {"x": 1593.84, "y": 188.76, "z": 14.37}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1590.0, "y": 174.37, "z": 14.8}, {"x": 1596.83, "y": 187.23, "z": 14.42}], "right_lane_mark_type": "NONE", "successors": [42819650], "predecessors": [42818485], "right_neighbor_id": null, "left_neighbor_id": 42819408}, "42819427": {"id": 42819427, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1590.73, "y": 190.34, "z": 14.47}, {"x": 1593.93, "y": 204.21, "z": 14.02}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1593.84, "y": 188.76, "z": 14.37}, {"x": 1597.04, "y": 202.52, "z": 14.01}], "right_lane_mark_type": "NONE", "successors": [42818858], "predecessors": [42819408], "right_neighbor_id": null, "left_neighbor_id": null}, "42819444": {"id": 42819444, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1601.14, "y": 226.04, "z": 13.24}, {"x": 1591.14, "y": 205.77, "z": 13.97}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1598.06, "y": 227.53, "z": 13.19}, {"x": 1592.07, "y": 215.57, "z": 13.72}], "right_lane_mark_type": "NONE", "successors": [42820396], "predecessors": [42807338, 42809414], "right_neighbor_id": null, "left_neighbor_id": 42819224}, "42819648": {"id": 42819648, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1599.88, "y": 201.47, "z": 13.96}, {"x": 1610.52, "y": 222.0, "z": 13.42}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1603.28, "y": 200.19, "z": 13.98}, {"x": 1611.21, "y": 216.17, "z": 13.57}, {"x": 1611.3, "y": 216.25, "z": 13.57}, {"x": 1613.52, "y": 220.89, "z": 13.38}], "right_lane_mark_type": "NONE", "successors": [42807783, 42808640], "predecessors": [], "right_neighbor_id": null, "left_neighbor_id": 42819649}, "42819649": {"id": 42819649, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1597.04, "y": 202.52, "z": 14.01}, {"x": 1607.54, "y": 223.3, "z": 13.42}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1599.88, "y": 201.47, "z": 13.96}, {"x": 1610.52, "y": 222.0, "z": 13.42}], "right_lane_mark_type": "DASHED_WHITE", "successors": [42812208], "predecessors": [42819006], "right_neighbor_id": 42819648, "left_neighbor_id": 42818858}, "42819650": {"id": 42819650, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1593.84, "y": 188.76, "z": 14.37}, {"x": 1599.88, "y": 201.47, "z": 13.96}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1596.83, "y": 187.23, "z": 14.42}, {"x": 1603.28, "y": 200.19, "z": 13.98}], "right_lane_mark_type": "NONE", "successors": [42819648], "predecessors": [42819421], "right_neighbor_id": null, "left_neighbor_id": 42819006}, "42819897": {"id": 42819897, "is_intersection": true, "lane_type": "BUS", "left_lane_boundary": [{"x": 1601.93, "y": 249.85, "z": 12.6}, {"x": 1626.46, "y": 232.77, "z": 13.16}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1600.38, "y": 246.57, "z": 12.62}, {"x": 1600.81, "y": 246.38, "z": 12.62}, {"x": 1601.79, "y": 245.84, "z": 12.64}, {"x": 1607.17, "y": 241.21, "z": 12.83}, {"x": 1623.78, "y": 228.03, "z": 13.25}], "right_lane_mark_type": "NONE", "successors": [42819691], "predecessors": [42811336], "right_neighbor_id": 42807784, "left_neighbor_id": null}, "42820396": {"id": 42820396, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1591.14, "y": 205.77, "z": 13.97}, {"x": 1590.0, "y": 203.32, "z": 14.06}], "left_lane_mark_type": "DASHED_WHITE", "right_lane_boundary": [{"x": 1592.07, "y": 215.57, "z": 13.72}, {"x": 1590.0, "y": 211.38, "z": 13.85}], "right_lane_mark_type": "NONE", "successors": [42818027], "predecessors": [42819444], "right_neighbor_id": null, "left_neighbor_id": 42818861}, "42824232": {"id": 42824232, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1463.31, "y": 112.03, "z": 15.21}, {"x": 1462.27, "y": 110.1, "z": 15.27}, {"x": 1461.34, "y": 108.3, "z": 15.31}, {"x": 1460.49, "y": 106.6, "z": 15.37}, {"x": 1459.74, "y": 105.03, "z": 15.42}, {"x": 1459.07, "y": 103.55, "z": 15.48}, {"x": 1458.48, "y": 102.19, "z": 15.55}, {"x": 1457.97, "y": 100.92, "z": 15.62}, {"x": 1457.54, "y": 99.75, "z": 15.68}, {"x": 1457.19, "y": 98.66, "z": 15.74}, {"x": 1456.9, "y": 97.66, "z": 15.79}, {"x": 1456.69, "y": 96.74, "z": 15.82}, {"x": 1456.54, "y": 95.89, "z": 15.85}, {"x": 1456.45, "y": 95.12, "z": 15.86}, {"x": 1456.43, "y": 94.41, "z": 15.88}, {"x": 1456.46, "y": 93.76, "z": 15.9}, {"x": 1456.54, "y": 93.17, "z": 15.92}, {"x": 1456.68, "y": 92.63, "z": 15.94}, {"x": 1456.86, "y": 92.13, "z": 15.96}, {"x": 1457.09, "y": 91.68, "z": 15.97}, {"x": 1457.37, "y": 91.27, "z": 15.98}, {"x": 1457.68, "y": 90.88, "z": 16.0}, {"x": 1458.03, "y": 90.53, "z": 16.02}, {"x": 1458.41, "y": 90.2, "z": 16.03}, {"x": 1458.82, "y": 89.89, "z": 16.05}, {"x": 1459.27, "y": 89.59, "z": 16.07}, {"x": 1459.73, "y": 89.3, "z": 16.09}, {"x": 1460.22, "y": 89.02, "z": 16.11}, {"x": 1460.73, "y": 88.74, "z": 16.13}, {"x": 1461.26, "y": 88.45, "z": 16.16}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1458.26, "y": 114.47, "z": 15.23}, {"x": 1457.56, "y": 113.21, "z": 15.29}, {"x": 1456.9, "y": 111.97, "z": 15.31}, {"x": 1456.27, "y": 110.76, "z": 15.34}, {"x": 1455.68, "y": 109.58, "z": 15.39}, {"x": 1455.12, "y": 108.42, "z": 15.41}, {"x": 1454.6, "y": 107.29, "z": 15.44}, {"x": 1454.12, "y": 106.18, "z": 15.46}, {"x": 1453.68, "y": 105.1, "z": 15.49}, {"x": 1453.28, "y": 104.05, "z": 15.53}, {"x": 1452.92, "y": 103.02, "z": 15.56}, {"x": 1452.6, "y": 102.02, "z": 15.6}, {"x": 1452.32, "y": 101.04, "z": 15.63}, {"x": 1452.08, "y": 100.09, "z": 15.66}, {"x": 1451.89, "y": 99.17, "z": 15.7}, {"x": 1451.73, "y": 98.27, "z": 15.72}, {"x": 1451.62, "y": 97.4, "z": 15.75}, {"x": 1451.56, "y": 96.55, "z": 15.78}, {"x": 1451.54, "y": 95.73, "z": 15.81}, {"x": 1451.57, "y": 94.93, "z": 15.84}, {"x": 1451.64, "y": 94.16, "z": 15.86}, {"x": 1451.76, "y": 93.41, "z": 15.88}, {"x": 1451.93, "y": 92.69, "z": 15.89}, {"x": 1452.15, "y": 91.99, "z": 15.91}, {"x": 1452.41, "y": 91.32, "z": 15.92}, {"x": 1452.73, "y": 90.67, "z": 15.93}, {"x": 1453.1, "y": 90.05, "z": 15.95}, {"x": 1453.51, "y": 89.45, "z": 15.96}, {"x": 1453.98, "y": 88.87, "z": 15.98}, {"x": 1454.5, "y": 88.32, "z": 16.0}, {"x": 1455.08, "y": 87.8, "z": 16.02}, {"x": 1455.71, "y": 87.3, "z": 16.05}, {"x": 1456.39, "y": 86.82, "z": 16.08}, {"x": 1457.13, "y": 86.37, "z": 16.11}, {"x": 1457.92, "y": 85.94, "z": 16.14}, {"x": 1458.77, "y": 85.54, "z": 16.17}, {"x": 1459.68, "y": 85.16, "z": 16.21}], "right_lane_mark_type": "NONE", "successors": [42816194], "predecessors": [42816397], "right_neighbor_id": null, "left_neighbor_id": null}, "42824242": {"id": 42824242, "is_intersection": true, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1463.31, "y": 112.03, "z": 15.21}, {"x": 1459.27, "y": 103.93, "z": 15.47}, {"x": 1453.23, "y": 85.61, "z": 15.99}, {"x": 1451.4, "y": 81.91, "z": 16.03}, {"x": 1451.29, "y": 81.75, "z": 16.04}, {"x": 1451.06, "y": 81.23, "z": 16.06}, {"x": 1450.88, "y": 80.87, "z": 16.07}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1458.26, "y": 114.47, "z": 15.23}, {"x": 1449.17, "y": 90.16, "z": 15.92}, {"x": 1447.98, "y": 87.6, "z": 15.93}, {"x": 1445.72, "y": 83.34, "z": 16.0}], "right_lane_mark_type": "NONE", "successors": [42816930], "predecessors": [42816397], "right_neighbor_id": null, "left_neighbor_id": null}, "42844999": {"id": 42844999, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1440.0, "y": 254.41, "z": 12.34}, {"x": 1335.34, "y": 216.37, "z": 11.87}], "left_lane_mark_type": "NONE", "right_lane_boundary": [{"x": 1440.0, "y": 259.66, "z": 12.34}, {"x": 1345.44, "y": 225.97, "z": 11.93}, {"x": 1341.17, "y": 223.11, "z": 11.94}, {"x": 1333.78, "y": 220.55, "z": 11.84}], "right_lane_mark_type": "NONE", "successors": [42845550, 42844132, 42845365], "predecessors": [42806482], "right_neighbor_id": null, "left_neighbor_id": null}, "42915538": {"id": 42915538, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1581.88, "y": 255.77, "z": 12.55}, {"x": 1582.93, "y": 255.89, "z": 12.56}, {"x": 1587.1, "y": 255.94, "z": 12.55}, {"x": 1590.0, "y": 255.85, "z": 12.58}], "left_lane_mark_type": "SOLID_YELLOW", "right_lane_boundary": [{"x": 1582.28, "y": 252.36, "z": 12.54}, {"x": 1583.6, "y": 252.49, "z": 12.52}, {"x": 1586.31, "y": 252.69, "z": 12.52}, {"x": 1590.0, "y": 252.59, "z": 12.54}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42811541], "predecessors": [42807643], "right_neighbor_id": 42915544, "left_neighbor_id": null}, "42915541": {"id": 42915541, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1580.4, "y": 259.57, "z": 12.52}, {"x": 1578.6, "y": 259.01, "z": 12.49}, {"x": 1576.57, "y": 258.32, "z": 12.49}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1579.09, "y": 262.87, "z": 12.45}, {"x": 1575.47, "y": 261.72, "z": 12.46}], "right_lane_mark_type": "NONE", "successors": [42811323], "predecessors": [42811882], "right_neighbor_id": null, "left_neighbor_id": 42915647}, "42915544": {"id": 42915544, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1582.28, "y": 252.36, "z": 12.54}, {"x": 1583.6, "y": 252.49, "z": 12.52}, {"x": 1586.31, "y": 252.69, "z": 12.52}, {"x": 1590.0, "y": 252.59, "z": 12.54}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1582.22, "y": 249.21, "z": 12.51}, {"x": 1585.19, "y": 249.46, "z": 12.52}, {"x": 1587.7, "y": 249.4, "z": 12.52}, {"x": 1590.0, "y": 249.27, "z": 12.54}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42811336], "predecessors": [42812483], "right_neighbor_id": 42915650, "left_neighbor_id": 42915538}, "42915647": {"id": 42915647, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1581.47, "y": 256.42, "z": 12.53}, {"x": 1577.64, "y": 255.23, "z": 12.55}], "left_lane_mark_type": "DOUBLE_SOLID_YELLOW", "right_lane_boundary": [{"x": 1580.4, "y": 259.57, "z": 12.52}, {"x": 1578.6, "y": 259.01, "z": 12.49}, {"x": 1576.57, "y": 258.32, "z": 12.49}], "right_lane_mark_type": "SOLID_WHITE", "successors": [42811283], "predecessors": [42810830], "right_neighbor_id": 42915541, "left_neighbor_id": null}, "42915650": {"id": 42915650, "is_intersection": false, "lane_type": "VEHICLE", "left_lane_boundary": [{"x": 1582.22, "y": 249.21, "z": 12.51}, {"x": 1585.19, "y": 249.46, "z": 12.52}, {"x": 1587.7, "y": 249.4, "z": 12.52}, {"x": 1590.0, "y": 249.27, "z": 12.54}], "left_lane_mark_type": "SOLID_WHITE", "right_lane_boundary": [{"x": 1582.72, "y": 245.79, "z": 12.49}, {"x": 1584.98, "y": 245.94, "z": 12.47}, {"x": 1586.49, "y": 246.04, "z": 12.48}, {"x": 1588.1, "y": 246.04, "z": 12.49}, {"x": 1590.0, "y": 245.99, "z": 12.5}], "right_lane_mark_type": "NONE", "successors": [42811683], "predecessors": [42812494], "right_neighbor_id": null, "left_neighbor_id": 42915544}}, "drivable_areas": {"1414553": {"area_boundary": [{"x": 1438.32, "y": 309.98, "z": 11.66}, {"x": 1435.32, "y": 308.93, "z": 11.72}, {"x": 1434.84, "y": 308.82, "z": 11.72}, {"x": 1434.46, "y": 309.0, "z": 11.72}, {"x": 1433.96, "y": 309.56, "z": 11.74}, {"x": 1433.66, "y": 310.11, "z": 11.76}, {"x": 1432.17, "y": 314.1, "z": 11.81}, {"x": 1429.07, "y": 312.96, "z": 11.86}, {"x": 1430.66, "y": 308.91, "z": 11.77}, {"x": 1430.71, "y": 308.26, "z": 11.74}, {"x": 1430.46, "y": 307.51, "z": 11.7}, {"x": 1430.06, "y": 307.06, "z": 11.67}, {"x": 1393.68, "y": 294.05, "z": 11.84}, {"x": 1383.47, "y": 290.42, "z": 11.92}, {"x": 1375.03, "y": 287.39, "z": 11.94}, {"x": 1374.47, "y": 287.39, "z": 11.98}, {"x": 1374.02, "y": 287.69, "z": 12.05}, {"x": 1366.23, "y": 309.92, "z": 11.78}, {"x": 1360.78, "y": 309.87, "z": 11.78}, {"x": 1369.55, "y": 286.46, "z": 12.04}, {"x": 1369.67, "y": 286.05, "z": 12.0}, {"x": 1369.63, "y": 285.63, "z": 11.97}, {"x": 1369.42, "y": 285.38, "z": 11.95}, {"x": 1350.0, "y": 278.42, "z": 12.11}, {"x": 1350.0, "y": 271.6, "z": 12.15}, {"x": 1413.62, "y": 294.08, "z": 11.81}, {"x": 1414.0, "y": 294.08, "z": 11.8}, {"x": 1414.12, "y": 293.72, "z": 11.8}, {"x": 1413.86, "y": 293.52, "z": 11.8}, {"x": 1350.0, "y": 270.81, "z": 12.15}, {"x": 1350.0, "y": 267.65, "z": 12.14}, {"x": 1422.66, "y": 293.43, "z": 11.79}, {"x": 1423.16, "y": 293.53, "z": 11.79}, {"x": 1423.76, "y": 293.38, "z": 11.8}, {"x": 1424.26, "y": 292.88, "z": 11.86}, {"x": 1425.26, "y": 290.03, "z": 12.07}, {"x": 1427.75, "y": 290.87, "z": 12.11}, {"x": 1426.85, "y": 293.57, "z": 11.91}, {"x": 1426.75, "y": 294.32, "z": 11.82}, {"x": 1427.0, "y": 294.87, "z": 11.73}, {"x": 1427.5, "y": 295.17, "z": 11.78}, {"x": 1440.0, "y": 299.62, "z": 11.73}, {"x": 1440.0, "y": 302.96, "z": 11.71}, {"x": 1428.29, "y": 298.73, "z": 11.76}, {"x": 1427.93, "y": 298.71, "z": 11.76}, {"x": 1427.83, "y": 298.97, "z": 11.76}, {"x": 1428.08, "y": 299.21, "z": 11.76}, {"x": 1440.0, "y": 303.5, "z": 11.71}, {"x": 1440.0, "y": 310.44, "z": 11.6}], "id": 1414553}, "1414512": {"area_boundary": [{"x": 1590.0, "y": 173.38, "z": 14.91}, {"x": 1580.19, "y": 153.78, "z": 15.57}, {"x": 1563.47, "y": 120.0, "z": 16.81}, {"x": 1544.61, "y": 120.0, "z": 17.14}, {"x": 1561.61, "y": 154.32, "z": 15.86}, {"x": 1567.7, "y": 166.73, "z": 15.39}, {"x": 1575.97, "y": 183.16, "z": 14.75}, {"x": 1581.99, "y": 179.85, "z": 14.91}, {"x": 1582.15, "y": 179.3, "z": 14.95}, {"x": 1582.66, "y": 178.94, "z": 15.0}, {"x": 1583.65, "y": 178.46, "z": 15.01}, {"x": 1584.12, "y": 178.36, "z": 15.0}, {"x": 1584.52, "y": 178.44, "z": 14.95}], "id": 1414512}, "1414238": {"area_boundary": [{"x": 1392.34, "y": 196.78, "z": 12.86}, {"x": 1384.48, "y": 193.95, "z": 12.94}, {"x": 1377.31, "y": 191.42, "z": 12.99}, {"x": 1369.56, "y": 188.71, "z": 13.02}, {"x": 1361.95, "y": 186.04, "z": 13.04}, {"x": 1349.77, "y": 181.64, "z": 13.18}, {"x": 1348.73, "y": 181.3, "z": 13.16}, {"x": 1348.1, "y": 181.18, "z": 13.15}, {"x": 1347.51, "y": 181.17, "z": 13.15}, {"x": 1346.82, "y": 181.28, "z": 13.14}, {"x": 1346.31, "y": 181.44, "z": 13.13}, {"x": 1345.78, "y": 181.81, "z": 13.11}, {"x": 1345.35, "y": 182.11, "z": 13.1}, {"x": 1344.94, "y": 182.48, "z": 13.12}, {"x": 1344.53, "y": 182.92, "z": 13.11}, {"x": 1344.08, "y": 183.81, "z": 13.12}, {"x": 1338.87, "y": 198.96, "z": 12.82}, {"x": 1333.33, "y": 214.07, "z": 11.99}, {"x": 1333.15, "y": 214.63, "z": 11.95}, {"x": 1333.18, "y": 215.09, "z": 11.92}, {"x": 1333.41, "y": 215.53, "z": 11.9}, {"x": 1333.81, "y": 215.81, "z": 11.88}, {"x": 1440.0, "y": 254.41, "z": 12.34}, {"x": 1440.0, "y": 259.66, "z": 12.34}, {"x": 1345.39, "y": 225.95, "z": 11.93}, {"x": 1341.34, "y": 223.28, "z": 11.94}, {"x": 1341.02, "y": 223.11, "z": 11.94}, {"x": 1332.37, "y": 220.05, "z": 11.85}, {"x": 1331.73, "y": 220.08, "z": 11.86}, {"x": 1331.17, "y": 220.29, "z": 11.87}, {"x": 1330.83, "y": 220.63, "z": 11.87}, {"x": 1330.58, "y": 221.11, "z": 11.87}, {"x": 1323.76, "y": 240.0, "z": 12.39}, {"x": 1319.9, "y": 240.0, "z": 12.33}, {"x": 1327.05, "y": 220.67, "z": 11.88}, {"x": 1327.21, "y": 220.08, "z": 11.87}, {"x": 1327.23, "y": 219.59, "z": 11.87}, {"x": 1326.97, "y": 219.14, "z": 11.87}, {"x": 1326.58, "y": 218.89, "z": 11.91}, {"x": 1324.86, "y": 218.32, "z": 11.95}, {"x": 1323.85, "y": 218.74, "z": 12.03}, {"x": 1303.83, "y": 211.69, "z": 12.58}, {"x": 1303.27, "y": 211.56, "z": 12.61}, {"x": 1302.86, "y": 211.58, "z": 12.64}, {"x": 1302.45, "y": 211.99, "z": 12.69}, {"x": 1300.17, "y": 218.52, "z": 12.86}, {"x": 1293.24, "y": 215.78, "z": 12.92}, {"x": 1295.6, "y": 209.52, "z": 12.83}, {"x": 1295.72, "y": 209.05, "z": 12.81}, {"x": 1295.68, "y": 208.39, "z": 12.81}, {"x": 1295.37, "y": 207.92, "z": 12.82}, {"x": 1294.37, "y": 207.42, "z": 12.83}, {"x": 1290.0, "y": 205.85, "z": 12.88}, {"x": 1290.0, "y": 199.75, "z": 12.94}, {"x": 1297.97, "y": 202.47, "z": 12.87}, {"x": 1298.48, "y": 202.4, "z": 12.88}, {"x": 1298.77, "y": 202.08, "z": 12.92}, {"x": 1299.75, "y": 199.93, "z": 12.89}, {"x": 1302.51, "y": 192.16, "z": 12.9}, {"x": 1311.95, "y": 195.77, "z": 12.74}, {"x": 1308.67, "y": 204.92, "z": 12.76}, {"x": 1308.8, "y": 205.51, "z": 12.69}, {"x": 1309.14, "y": 205.98, "z": 12.62}, {"x": 1328.34, "y": 212.97, "z": 11.98}, {"x": 1328.78, "y": 213.02, "z": 11.97}, {"x": 1329.18, "y": 212.9, "z": 11.97}, {"x": 1329.43, "y": 212.5, "z": 11.99}, {"x": 1334.97, "y": 197.73, "z": 12.72}, {"x": 1340.04, "y": 183.05, "z": 13.03}, {"x": 1340.45, "y": 181.63, "z": 13.1}, {"x": 1340.66, "y": 180.73, "z": 13.1}, {"x": 1340.64, "y": 180.13, "z": 13.13}, {"x": 1340.62, "y": 179.39, "z": 13.15}, {"x": 1340.42, "y": 178.9, "z": 13.18}, {"x": 1339.98, "y": 178.45, "z": 13.2}, {"x": 1339.26, "y": 177.95, "z": 13.24}, {"x": 1337.92, "y": 177.3, "z": 13.22}, {"x": 1328.17, "y": 173.94, "z": 13.39}, {"x": 1312.63, "y": 168.41, "z": 13.54}, {"x": 1290.0, "y": 160.66, "z": 13.73}, {"x": 1290.0, "y": 142.72, "z": 13.91}, {"x": 1311.18, "y": 150.0, "z": 13.81}, {"x": 1312.12, "y": 150.22, "z": 13.81}, {"x": 1312.88, "y": 150.29, "z": 13.8}, {"x": 1313.51, "y": 150.24, "z": 13.8}, {"x": 1314.22, "y": 150.15, "z": 13.8}, {"x": 1314.84, "y": 149.99, "z": 13.8}, {"x": 1315.69, "y": 149.72, "z": 13.83}, {"x": 1316.65, "y": 149.38, "z": 13.86}, {"x": 1317.45, "y": 149.08, "z": 13.85}, {"x": 1318.35, "y": 148.63, "z": 13.86}, {"x": 1321.01, "y": 147.39, "z": 13.92}, {"x": 1350.0, "y": 133.71, "z": 14.28}, {"x": 1350.0, "y": 145.18, "z": 13.96}, {"x": 1335.61, "y": 152.28, "z": 13.78}, {"x": 1334.91, "y": 152.98, "z": 13.81}, {"x": 1334.49, "y": 153.75, "z": 13.78}, {"x": 1334.21, "y": 154.46, "z": 13.75}, {"x": 1334.07, "y": 155.03, "z": 13.73}, {"x": 1334.04, "y": 155.62, "z": 13.69}, {"x": 1334.07, "y": 156.24, "z": 13.67}, {"x": 1334.15, "y": 156.79, "z": 13.66}, {"x": 1334.36, "y": 157.25, "z": 13.66}, {"x": 1334.68, "y": 157.75, "z": 13.65}, {"x": 1335.08, "y": 158.2, "z": 13.64}, {"x": 1335.63, "y": 158.54, "z": 13.65}, {"x": 1336.58, "y": 158.94, "z": 13.65}, {"x": 1341.06, "y": 160.53, "z": 13.61}, {"x": 1351.0, "y": 164.11, "z": 13.51}, {"x": 1371.55, "y": 171.4, "z": 13.26}, {"x": 1372.77, "y": 171.77, "z": 13.21}, {"x": 1373.66, "y": 171.96, "z": 13.18}, {"x": 1374.17, "y": 171.99, "z": 13.18}, {"x": 1374.75, "y": 171.85, "z": 13.19}, {"x": 1375.29, "y": 171.62, "z": 13.2}, {"x": 1375.89, "y": 171.3, "z": 13.23}, {"x": 1376.22, "y": 170.96, "z": 13.25}, {"x": 1376.6, "y": 170.49, "z": 13.29}, {"x": 1376.94, "y": 169.98, "z": 13.31}, {"x": 1377.19, "y": 169.46, "z": 13.35}, {"x": 1377.31, "y": 168.98, "z": 13.4}, {"x": 1377.4, "y": 168.36, "z": 13.42}, {"x": 1377.43, "y": 167.74, "z": 13.45}, {"x": 1377.4, "y": 166.99, "z": 13.48}, {"x": 1376.98, "y": 166.21, "z": 13.48}, {"x": 1375.01, "y": 162.54, "z": 13.61}, {"x": 1368.79, "y": 150.0, "z": 13.93}, {"x": 1380.87, "y": 150.0, "z": 13.95}, {"x": 1392.14, "y": 172.24, "z": 13.42}, {"x": 1393.63, "y": 175.46, "z": 13.31}, {"x": 1394.21, "y": 176.57, "z": 13.27}, {"x": 1394.64, "y": 177.37, "z": 13.24}, {"x": 1395.18, "y": 178.16, "z": 13.19}, {"x": 1395.66, "y": 178.81, "z": 13.16}, {"x": 1396.11, "y": 179.33, "z": 13.14}, {"x": 1396.73, "y": 179.82, "z": 13.13}, {"x": 1397.6, "y": 180.36, "z": 13.15}, {"x": 1398.49, "y": 180.83, "z": 13.15}, {"x": 1399.49, "y": 181.32, "z": 13.16}, {"x": 1406.41, "y": 183.91, "z": 13.07}, {"x": 1412.46, "y": 186.03, "z": 13.05}, {"x": 1417.58, "y": 187.91, "z": 13.02}, {"x": 1431.82, "y": 192.95, "z": 12.99}, {"x": 1440.0, "y": 195.77, "z": 12.95}, {"x": 1440.0, "y": 213.68, "z": 12.74}], "id": 1414238}, "1413643": {"area_boundary": [{"x": 1498.81, "y": 240.0, "z": 12.25}, {"x": 1498.87, "y": 239.86, "z": 12.25}, {"x": 1499.35, "y": 238.93, "z": 12.2}, {"x": 1499.85, "y": 238.13, "z": 12.19}, {"x": 1500.57, "y": 237.15, "z": 12.16}, {"x": 1501.25, "y": 236.58, "z": 12.17}, {"x": 1502.11, "y": 236.28, "z": 12.19}, {"x": 1503.28, "y": 236.29, "z": 12.21}, {"x": 1504.59, "y": 236.65, "z": 12.23}, {"x": 1507.21, "y": 237.51, "z": 12.27}, {"x": 1575.51, "y": 261.94, "z": 12.47}, {"x": 1578.27, "y": 262.85, "z": 12.46}, {"x": 1580.38, "y": 263.48, "z": 12.46}, {"x": 1583.23, "y": 264.11, "z": 12.53}, {"x": 1585.56, "y": 264.48, "z": 12.55}, {"x": 1588.15, "y": 264.78, "z": 12.53}, {"x": 1590.0, "y": 264.73, "z": 12.52}, {"x": 1590.0, "y": 257.54, "z": 12.58}, {"x": 1588.84, "y": 257.3, "z": 12.6}, {"x": 1587.9, "y": 257.26, "z": 12.62}, {"x": 1586.67, "y": 257.19, "z": 12.61}, {"x": 1585.11, "y": 257.02, "z": 12.56}, {"x": 1583.7, "y": 256.8, "z": 12.53}, {"x": 1583.01, "y": 256.65, "z": 12.52}, {"x": 1582.51, "y": 256.55, "z": 12.53}, {"x": 1582.35, "y": 256.18, "z": 12.54}, {"x": 1582.51, "y": 256.02, "z": 12.55}, {"x": 1583.04, "y": 255.98, "z": 12.56}, {"x": 1583.91, "y": 256.09, "z": 12.56}, {"x": 1584.84, "y": 256.18, "z": 12.55}, {"x": 1586.41, "y": 256.27, "z": 12.57}, {"x": 1588.22, "y": 256.24, "z": 12.57}, {"x": 1590.0, "y": 256.06, "z": 12.58}, {"x": 1590.0, "y": 245.82, "z": 12.51}, {"x": 1587.36, "y": 245.81, "z": 12.51}, {"x": 1584.72, "y": 245.71, "z": 12.5}, {"x": 1582.3, "y": 245.51, "z": 12.51}, {"x": 1579.85, "y": 245.11, "z": 12.48}, {"x": 1576.17, "y": 244.11, "z": 12.46}, {"x": 1573.64, "y": 243.15, "z": 12.47}, {"x": 1565.35, "y": 240.13, "z": 12.49}, {"x": 1558.3, "y": 237.65, "z": 12.51}, {"x": 1553.2, "y": 235.92, "z": 12.5}, {"x": 1541.62, "y": 231.8, "z": 12.52}, {"x": 1527.48, "y": 226.8, "z": 12.55}, {"x": 1519.5, "y": 223.94, "z": 12.59}, {"x": 1512.17, "y": 221.39, "z": 12.64}, {"x": 1510.84, "y": 220.83, "z": 12.59}, {"x": 1510.04, "y": 220.32, "z": 12.62}, {"x": 1509.11, "y": 219.27, "z": 12.59}, {"x": 1508.69, "y": 218.42, "z": 12.61}, {"x": 1508.34, "y": 217.48, "z": 12.63}, {"x": 1508.1, "y": 216.32, "z": 12.67}, {"x": 1508.2, "y": 214.97, "z": 12.69}, {"x": 1508.68, "y": 213.37, "z": 12.76}, {"x": 1509.93, "y": 209.72, "z": 12.81}, {"x": 1510.72, "y": 205.33, "z": 12.86}, {"x": 1510.84, "y": 203.49, "z": 12.86}, {"x": 1510.76, "y": 201.83, "z": 12.9}, {"x": 1510.54, "y": 200.8, "z": 12.92}, {"x": 1510.21, "y": 199.33, "z": 12.94}, {"x": 1509.74, "y": 197.55, "z": 13.01}, {"x": 1509.1, "y": 195.94, "z": 12.96}, {"x": 1508.46, "y": 194.49, "z": 13.0}, {"x": 1507.61, "y": 192.79, "z": 12.99}, {"x": 1505.37, "y": 188.48, "z": 13.03}, {"x": 1497.47, "y": 173.06, "z": 13.44}, {"x": 1483.76, "y": 145.76, "z": 14.12}, {"x": 1470.79, "y": 120.0, "z": 14.92}, {"x": 1460.69, "y": 120.0, "z": 15.1}, {"x": 1496.18, "y": 190.69, "z": 13.13}, {"x": 1498.52, "y": 195.13, "z": 13.03}, {"x": 1498.86, "y": 196.25, "z": 12.98}, {"x": 1499.02, "y": 197.29, "z": 12.97}, {"x": 1499.14, "y": 198.38, "z": 12.95}, {"x": 1499.18, "y": 199.59, "z": 12.92}, {"x": 1498.98, "y": 200.66, "z": 12.9}, {"x": 1498.68, "y": 201.79, "z": 12.89}, {"x": 1497.22, "y": 205.87, "z": 12.83}, {"x": 1494.89, "y": 211.71, "z": 12.72}, {"x": 1494.44, "y": 212.59, "z": 12.69}, {"x": 1494.05, "y": 213.16, "z": 12.66}, {"x": 1493.66, "y": 213.56, "z": 12.65}, {"x": 1492.88, "y": 213.9, "z": 12.64}, {"x": 1491.96, "y": 214.1, "z": 12.64}, {"x": 1491.21, "y": 214.1, "z": 12.67}, {"x": 1490.04, "y": 213.75, "z": 12.66}, {"x": 1461.88, "y": 203.58, "z": 12.83}, {"x": 1440.0, "y": 195.77, "z": 12.95}, {"x": 1440.0, "y": 213.68, "z": 12.74}, {"x": 1442.88, "y": 214.67, "z": 12.71}, {"x": 1457.91, "y": 220.15, "z": 12.58}, {"x": 1480.64, "y": 228.14, "z": 12.48}, {"x": 1484.64, "y": 229.59, "z": 12.46}, {"x": 1485.78, "y": 230.29, "z": 12.42}, {"x": 1486.56, "y": 231.06, "z": 12.4}, {"x": 1486.87, "y": 231.62, "z": 12.4}, {"x": 1487.01, "y": 232.36, "z": 12.39}, {"x": 1487.07, "y": 233.38, "z": 12.37}, {"x": 1486.35, "y": 235.74, "z": 12.31}, {"x": 1486.17, "y": 236.84, "z": 12.26}, {"x": 1486.58, "y": 237.22, "z": 12.25}, {"x": 1487.14, "y": 237.44, "z": 12.25}, {"x": 1487.52, "y": 237.89, "z": 12.25}, {"x": 1487.4, "y": 238.71, "z": 12.24}, {"x": 1486.95, "y": 240.0, "z": 12.23}], "id": 1413643}, "1413634": {"area_boundary": [{"x": 1590.0, "y": 211.38, "z": 13.85}, {"x": 1575.97, "y": 183.16, "z": 14.75}, {"x": 1581.99, "y": 179.85, "z": 14.91}, {"x": 1586.92, "y": 189.67, "z": 14.53}, {"x": 1587.17, "y": 190.02, "z": 14.53}, {"x": 1587.33, "y": 190.29, "z": 14.53}, {"x": 1587.63, "y": 190.46, "z": 14.54}, {"x": 1588.06, "y": 190.46, "z": 14.55}, {"x": 1588.49, "y": 190.3, "z": 14.55}, {"x": 1589.02, "y": 189.95, "z": 14.54}, {"x": 1589.64, "y": 189.33, "z": 14.52}, {"x": 1589.72, "y": 188.82, "z": 14.52}, {"x": 1589.12, "y": 187.33, "z": 14.54}, {"x": 1588.16, "y": 185.36, "z": 14.63}, {"x": 1586.34, "y": 181.79, "z": 14.74}, {"x": 1585.04, "y": 179.02, "z": 14.89}, {"x": 1584.52, "y": 178.44, "z": 14.95}, {"x": 1590.0, "y": 173.38, "z": 14.91}], "id": 1413634}, "1413633": {"area_boundary": [{"x": 1590.0, "y": 264.73, "z": 12.52}, {"x": 1591.67, "y": 264.68, "z": 12.55}, {"x": 1593.01, "y": 264.62, "z": 12.57}, {"x": 1595.08, "y": 264.43, "z": 12.53}, {"x": 1596.55, "y": 264.26, "z": 12.5}, {"x": 1598.03, "y": 263.93, "z": 12.51}, {"x": 1599.41, "y": 263.49, "z": 12.5}, {"x": 1600.88, "y": 263.08, "z": 12.49}, {"x": 1602.31, "y": 262.78, "z": 12.5}, {"x": 1604.25, "y": 262.46, "z": 12.46}, {"x": 1605.74, "y": 262.31, "z": 12.43}, {"x": 1607.13, "y": 262.28, "z": 12.4}, {"x": 1608.49, "y": 262.36, "z": 12.37}, {"x": 1610.86, "y": 262.7, "z": 12.35}, {"x": 1612.82, "y": 263.17, "z": 12.24}, {"x": 1615.34, "y": 263.94, "z": 12.16}, {"x": 1617.53, "y": 264.95, "z": 12.15}, {"x": 1619.91, "y": 266.12, "z": 12.04}, {"x": 1620.99, "y": 266.92, "z": 12.04}, {"x": 1622.44, "y": 268.07, "z": 12.01}, {"x": 1624.51, "y": 270.21, "z": 11.93}, {"x": 1626.17, "y": 272.15, "z": 11.86}, {"x": 1627.97, "y": 274.05, "z": 11.77}, {"x": 1629.32, "y": 275.71, "z": 11.75}, {"x": 1630.41, "y": 277.23, "z": 11.68}, {"x": 1631.18, "y": 278.74, "z": 11.6}, {"x": 1637.69, "y": 273.82, "z": 11.65}, {"x": 1629.84, "y": 265.18, "z": 12.0}, {"x": 1628.48, "y": 263.56, "z": 12.12}, {"x": 1627.89, "y": 262.77, "z": 12.13}, {"x": 1627.57, "y": 262.21, "z": 12.16}, {"x": 1627.64, "y": 261.79, "z": 12.18}, {"x": 1628.1, "y": 261.52, "z": 12.18}, {"x": 1628.43, "y": 261.7, "z": 12.16}, {"x": 1630.65, "y": 263.94, "z": 12.05}, {"x": 1632.1, "y": 265.48, "z": 12.01}, {"x": 1635.29, "y": 268.63, "z": 11.87}, {"x": 1637.77, "y": 271.06, "z": 11.69}, {"x": 1639.47, "y": 272.79, "z": 11.68}, {"x": 1647.84, "y": 266.32, "z": 11.48}, {"x": 1642.93, "y": 261.53, "z": 11.74}, {"x": 1638.03, "y": 256.67, "z": 12.04}, {"x": 1634.57, "y": 253.21, "z": 12.27}, {"x": 1633.62, "y": 251.96, "z": 12.33}, {"x": 1633.15, "y": 251.06, "z": 12.31}, {"x": 1632.72, "y": 249.91, "z": 12.34}, {"x": 1632.5, "y": 248.56, "z": 12.42}, {"x": 1632.35, "y": 247.15, "z": 12.55}, {"x": 1632.43, "y": 245.82, "z": 12.64}, {"x": 1632.69, "y": 244.7, "z": 12.72}, {"x": 1633.27, "y": 243.23, "z": 12.76}, {"x": 1633.85, "y": 242.18, "z": 12.86}, {"x": 1634.58, "y": 241.16, "z": 12.91}, {"x": 1636.16, "y": 239.59, "z": 13.04}, {"x": 1640.02, "y": 236.68, "z": 13.32}, {"x": 1635.0, "y": 230.45, "z": 13.56}, {"x": 1632.85, "y": 231.92, "z": 13.31}, {"x": 1630.54, "y": 233.48, "z": 13.17}, {"x": 1628.8, "y": 234.61, "z": 13.09}, {"x": 1627.84, "y": 235.15, "z": 13.03}, {"x": 1626.97, "y": 235.15, "z": 13.05}, {"x": 1626.4, "y": 234.83, "z": 13.04}, {"x": 1626.0, "y": 234.2, "z": 13.09}, {"x": 1625.99, "y": 233.58, "z": 13.1}, {"x": 1626.52, "y": 232.8, "z": 13.17}, {"x": 1630.82, "y": 230.01, "z": 13.39}, {"x": 1633.11, "y": 228.6, "z": 13.51}, {"x": 1629.11, "y": 220.1, "z": 13.55}, {"x": 1620.6, "y": 222.83, "z": 13.3}, {"x": 1619.55, "y": 222.98, "z": 13.29}, {"x": 1618.89, "y": 223.03, "z": 13.3}, {"x": 1618.18, "y": 222.84, "z": 13.31}, {"x": 1617.85, "y": 222.32, "z": 13.35}, {"x": 1617.88, "y": 221.8, "z": 13.39}, {"x": 1618.39, "y": 221.36, "z": 13.48}, {"x": 1619.36, "y": 220.67, "z": 13.52}, {"x": 1623.21, "y": 218.81, "z": 13.01}, {"x": 1620.66, "y": 213.17, "z": 13.32}, {"x": 1615.54, "y": 215.28, "z": 13.78}, {"x": 1612.16, "y": 216.55, "z": 13.62}, {"x": 1611.63, "y": 216.52, "z": 13.58}, {"x": 1611.21, "y": 216.17, "z": 13.57}, {"x": 1607.86, "y": 209.45, "z": 13.72}, {"x": 1590.0, "y": 173.38, "z": 14.91}, {"x": 1590.0, "y": 211.38, "z": 13.85}, {"x": 1599.76, "y": 231.0, "z": 13.04}, {"x": 1600.38, "y": 233.07, "z": 13.02}, {"x": 1600.52, "y": 235.09, "z": 12.92}, {"x": 1600.44, "y": 236.58, "z": 12.85}, {"x": 1600.06, "y": 238.2, "z": 12.82}, {"x": 1599.41, "y": 239.96, "z": 12.81}, {"x": 1598.53, "y": 241.38, "z": 12.74}, {"x": 1597.79, "y": 242.25, "z": 12.71}, {"x": 1596.68, "y": 243.36, "z": 12.67}, {"x": 1595.61, "y": 244.14, "z": 12.65}, {"x": 1594.58, "y": 244.74, "z": 12.63}, {"x": 1593.31, "y": 245.28, "z": 12.61}, {"x": 1592.05, "y": 245.61, "z": 12.57}, {"x": 1590.0, "y": 245.82, "z": 12.51}, {"x": 1590.0, "y": 256.06, "z": 12.58}, {"x": 1591.2, "y": 255.96, "z": 12.61}, {"x": 1592.42, "y": 255.82, "z": 12.63}, {"x": 1594.39, "y": 255.6, "z": 12.63}, {"x": 1595.86, "y": 255.34, "z": 12.6}, {"x": 1597.51, "y": 255.08, "z": 12.64}, {"x": 1598.8, "y": 254.82, "z": 12.67}, {"x": 1600.15, "y": 254.45, "z": 12.64}, {"x": 1601.46, "y": 254.08, "z": 12.66}, {"x": 1602.92, "y": 253.57, "z": 12.65}, {"x": 1603.71, "y": 253.29, "z": 12.63}, {"x": 1604.34, "y": 253.03, "z": 12.63}, {"x": 1604.68, "y": 252.99, "z": 12.58}, {"x": 1604.93, "y": 253.09, "z": 12.57}, {"x": 1604.99, "y": 253.33, "z": 12.57}, {"x": 1604.91, "y": 253.66, "z": 12.56}, {"x": 1604.63, "y": 253.93, "z": 12.58}, {"x": 1603.69, "y": 254.58, "z": 12.59}, {"x": 1601.83, "y": 255.61, "z": 12.61}, {"x": 1600.8, "y": 256.01, "z": 12.57}, {"x": 1599.55, "y": 256.29, "z": 12.6}, {"x": 1598.34, "y": 256.58, "z": 12.6}, {"x": 1595.41, "y": 257.0, "z": 12.58}, {"x": 1593.85, "y": 257.25, "z": 12.6}, {"x": 1591.66, "y": 257.44, "z": 12.6}, {"x": 1590.0, "y": 257.54, "z": 12.58}], "id": 1413633}, "1413629": {"area_boundary": [{"x": 1440.0, "y": 310.44, "z": 11.6}, {"x": 1449.7, "y": 313.94, "z": 11.44}, {"x": 1452.45, "y": 314.84, "z": 11.39}, {"x": 1452.97, "y": 315.05, "z": 11.35}, {"x": 1453.68, "y": 315.39, "z": 11.34}, {"x": 1454.47, "y": 316.02, "z": 11.31}, {"x": 1455.15, "y": 316.93, "z": 11.27}, {"x": 1455.52, "y": 317.69, "z": 11.25}, {"x": 1455.63, "y": 318.74, "z": 11.23}, {"x": 1455.57, "y": 319.65, "z": 11.21}, {"x": 1455.3, "y": 320.67, "z": 11.17}, {"x": 1449.99, "y": 335.53, "z": 10.65}, {"x": 1447.66, "y": 341.71, "z": 10.46}, {"x": 1447.02, "y": 343.4, "z": 10.4}, {"x": 1446.66, "y": 344.13, "z": 10.38}, {"x": 1446.37, "y": 344.49, "z": 10.37}, {"x": 1446.03, "y": 345.01, "z": 10.31}, {"x": 1445.47, "y": 345.73, "z": 10.31}, {"x": 1445.11, "y": 346.31, "z": 10.28}, {"x": 1444.78, "y": 346.73, "z": 10.27}, {"x": 1444.32, "y": 347.32, "z": 10.26}, {"x": 1443.92, "y": 347.78, "z": 10.21}, {"x": 1443.59, "y": 348.21, "z": 10.22}, {"x": 1443.18, "y": 348.65, "z": 10.2}, {"x": 1442.65, "y": 349.25, "z": 10.17}, {"x": 1441.98, "y": 349.92, "z": 10.12}, {"x": 1441.36, "y": 350.55, "z": 10.12}, {"x": 1440.88, "y": 350.98, "z": 10.06}, {"x": 1440.4, "y": 351.4, "z": 10.09}, {"x": 1446.47, "y": 354.93, "z": 9.99}, {"x": 1448.45, "y": 352.38, "z": 10.13}, {"x": 1449.4, "y": 351.25, "z": 10.22}, {"x": 1450.11, "y": 350.24, "z": 10.24}, {"x": 1450.97, "y": 349.11, "z": 10.34}, {"x": 1451.58, "y": 348.32, "z": 10.4}, {"x": 1452.31, "y": 347.39, "z": 10.41}, {"x": 1452.65, "y": 347.19, "z": 10.4}, {"x": 1452.98, "y": 347.24, "z": 10.41}, {"x": 1453.2, "y": 347.5, "z": 10.4}, {"x": 1453.16, "y": 348.31, "z": 10.37}, {"x": 1452.97, "y": 349.3, "z": 10.33}, {"x": 1452.84, "y": 350.25, "z": 10.3}, {"x": 1452.73, "y": 351.54, "z": 10.26}, {"x": 1452.66, "y": 352.75, "z": 10.21}, {"x": 1452.66, "y": 353.26, "z": 10.18}, {"x": 1452.67, "y": 353.88, "z": 10.16}, {"x": 1452.64, "y": 355.19, "z": 10.07}, {"x": 1452.54, "y": 356.65, "z": 10.01}, {"x": 1452.47, "y": 357.05, "z": 9.99}, {"x": 1459.32, "y": 358.04, "z": 9.78}, {"x": 1459.27, "y": 357.2, "z": 9.84}, {"x": 1459.25, "y": 356.19, "z": 9.91}, {"x": 1459.2, "y": 354.95, "z": 9.98}, {"x": 1459.21, "y": 353.5, "z": 10.08}, {"x": 1459.2, "y": 352.62, "z": 10.13}, {"x": 1459.14, "y": 351.33, "z": 10.19}, {"x": 1459.26, "y": 350.07, "z": 10.25}, {"x": 1459.61, "y": 348.03, "z": 10.43}, {"x": 1460.0, "y": 346.4, "z": 10.48}, {"x": 1460.62, "y": 344.46, "z": 10.59}, {"x": 1462.62, "y": 338.85, "z": 10.81}, {"x": 1465.12, "y": 331.95, "z": 10.98}, {"x": 1465.68, "y": 330.87, "z": 11.03}, {"x": 1466.29, "y": 329.87, "z": 11.03}, {"x": 1466.89, "y": 329.14, "z": 11.03}, {"x": 1467.38, "y": 328.5, "z": 11.05}, {"x": 1467.87, "y": 328.01, "z": 11.06}, {"x": 1468.3, "y": 327.65, "z": 11.07}, {"x": 1468.94, "y": 327.12, "z": 11.07}, {"x": 1469.87, "y": 326.59, "z": 11.09}, {"x": 1472.1, "y": 325.61, "z": 11.19}, {"x": 1474.78, "y": 324.57, "z": 11.25}, {"x": 1475.52, "y": 324.31, "z": 11.24}, {"x": 1476.31, "y": 324.13, "z": 11.23}, {"x": 1476.99, "y": 324.1, "z": 11.23}, {"x": 1477.5, "y": 324.15, "z": 11.22}, {"x": 1482.07, "y": 325.71, "z": 11.26}, {"x": 1496.03, "y": 330.62, "z": 11.28}, {"x": 1496.45, "y": 331.06, "z": 11.28}, {"x": 1496.79, "y": 331.58, "z": 11.3}, {"x": 1496.9, "y": 332.4, "z": 11.36}, {"x": 1496.88, "y": 333.27, "z": 11.4}, {"x": 1496.66, "y": 334.44, "z": 11.44}, {"x": 1496.39, "y": 335.59, "z": 11.48}, {"x": 1495.8, "y": 337.37, "z": 11.54}, {"x": 1500.85, "y": 339.01, "z": 11.65}, {"x": 1501.41, "y": 337.73, "z": 11.61}, {"x": 1501.71, "y": 337.21, "z": 11.58}, {"x": 1502.32, "y": 336.8, "z": 11.56}, {"x": 1502.91, "y": 336.6, "z": 11.54}, {"x": 1503.55, "y": 336.55, "z": 11.54}, {"x": 1504.15, "y": 336.35, "z": 11.53}, {"x": 1504.57, "y": 335.89, "z": 11.5}, {"x": 1505.16, "y": 335.27, "z": 11.46}, {"x": 1505.58, "y": 334.94, "z": 11.44}, {"x": 1506.18, "y": 334.69, "z": 11.38}, {"x": 1506.72, "y": 334.59, "z": 11.36}, {"x": 1509.53, "y": 335.45, "z": 11.37}, {"x": 1530.0, "y": 342.61, "z": 11.52}, {"x": 1530.0, "y": 335.57, "z": 11.55}, {"x": 1515.45, "y": 330.25, "z": 11.44}, {"x": 1477.16, "y": 316.74, "z": 11.29}, {"x": 1476.9, "y": 316.45, "z": 11.29}, {"x": 1477.0, "y": 316.16, "z": 11.29}, {"x": 1477.36, "y": 316.06, "z": 11.29}, {"x": 1515.45, "y": 329.48, "z": 11.44}, {"x": 1530.0, "y": 334.62, "z": 11.56}, {"x": 1530.0, "y": 331.75, "z": 11.57}, {"x": 1489.96, "y": 317.41, "z": 11.34}, {"x": 1477.64, "y": 312.98, "z": 11.33}, {"x": 1476.74, "y": 312.57, "z": 11.33}, {"x": 1475.59, "y": 311.82, "z": 11.32}, {"x": 1474.77, "y": 310.85, "z": 11.31}, {"x": 1474.35, "y": 309.87, "z": 11.32}, {"x": 1474.21, "y": 308.94, "z": 11.32}, {"x": 1474.23, "y": 308.11, "z": 11.35}, {"x": 1474.53, "y": 307.0, "z": 11.42}, {"x": 1474.9, "y": 306.04, "z": 11.46}, {"x": 1477.3, "y": 299.51, "z": 11.45}, {"x": 1479.32, "y": 293.96, "z": 11.46}, {"x": 1479.54, "y": 293.54, "z": 11.49}, {"x": 1479.96, "y": 293.4, "z": 11.54}, {"x": 1480.64, "y": 293.56, "z": 11.6}, {"x": 1488.03, "y": 295.89, "z": 11.87}, {"x": 1491.75, "y": 286.4, "z": 11.8}, {"x": 1483.96, "y": 283.75, "z": 11.65}, {"x": 1483.49, "y": 283.47, "z": 11.61}, {"x": 1483.33, "y": 283.19, "z": 11.59}, {"x": 1483.37, "y": 282.84, "z": 11.6}, {"x": 1483.51, "y": 282.51, "z": 11.62}, {"x": 1483.97, "y": 281.24, "z": 11.67}, {"x": 1487.46, "y": 271.69, "z": 11.75}, {"x": 1489.81, "y": 265.07, "z": 11.84}, {"x": 1490.1, "y": 264.21, "z": 11.78}, {"x": 1490.36, "y": 263.91, "z": 11.8}, {"x": 1490.73, "y": 263.72, "z": 11.81}, {"x": 1491.29, "y": 263.8, "z": 11.85}, {"x": 1500.0, "y": 266.55, "z": 11.93}, {"x": 1500.0, "y": 255.52, "z": 12.19}, {"x": 1494.54, "y": 253.92, "z": 12.0}, {"x": 1494.13, "y": 253.78, "z": 11.97}, {"x": 1494.06, "y": 253.5, "z": 11.96}, {"x": 1494.1, "y": 253.17, "z": 11.95}, {"x": 1498.81, "y": 240.0, "z": 12.25}, {"x": 1486.95, "y": 240.0, "z": 12.23}, {"x": 1485.39, "y": 244.44, "z": 12.16}, {"x": 1485.25, "y": 244.61, "z": 12.15}, {"x": 1484.96, "y": 244.75, "z": 12.14}, {"x": 1484.5, "y": 244.77, "z": 12.14}, {"x": 1484.01, "y": 244.72, "z": 12.13}, {"x": 1483.46, "y": 244.7, "z": 12.14}, {"x": 1483.06, "y": 244.86, "z": 12.13}, {"x": 1482.74, "y": 245.18, "z": 12.15}, {"x": 1475.65, "y": 264.88, "z": 11.87}, {"x": 1475.07, "y": 265.63, "z": 11.86}, {"x": 1474.2, "y": 265.75, "z": 11.87}, {"x": 1473.27, "y": 265.58, "z": 11.89}, {"x": 1440.0, "y": 254.41, "z": 12.34}, {"x": 1440.0, "y": 259.66, "z": 12.34}, {"x": 1471.86, "y": 271.07, "z": 11.83}, {"x": 1472.51, "y": 271.62, "z": 11.81}, {"x": 1472.71, "y": 272.67, "z": 11.78}, {"x": 1472.51, "y": 273.46, "z": 11.78}, {"x": 1462.94, "y": 300.03, "z": 11.41}, {"x": 1460.94, "y": 304.29, "z": 11.42}, {"x": 1460.52, "y": 304.75, "z": 11.42}, {"x": 1460.01, "y": 305.15, "z": 11.4}, {"x": 1459.39, "y": 305.41, "z": 11.42}, {"x": 1458.77, "y": 305.63, "z": 11.44}, {"x": 1458.07, "y": 305.77, "z": 11.45}, {"x": 1457.37, "y": 305.81, "z": 11.48}, {"x": 1456.77, "y": 305.71, "z": 11.5}, {"x": 1456.13, "y": 305.47, "z": 11.53}, {"x": 1455.28, "y": 305.13, "z": 11.52}, {"x": 1453.85, "y": 304.53, "z": 11.57}, {"x": 1444.06, "y": 301.0, "z": 11.69}, {"x": 1440.0, "y": 299.62, "z": 11.73}, {"x": 1440.0, "y": 302.96, "z": 11.71}, {"x": 1451.16, "y": 306.9, "z": 11.53}, {"x": 1451.44, "y": 307.17, "z": 11.52}, {"x": 1451.32, "y": 307.43, "z": 11.52}, {"x": 1450.96, "y": 307.4, "z": 11.53}, {"x": 1440.0, "y": 303.5, "z": 11.71}], "id": 1413629}, "1413627": {"area_boundary": [{"x": 1563.47, "y": 120.0, "z": 16.81}, {"x": 1539.29, "y": 71.05, "z": 18.62}, {"x": 1538.64, "y": 69.74, "z": 18.73}, {"x": 1537.99, "y": 68.55, "z": 18.71}, {"x": 1537.48, "y": 67.36, "z": 18.77}, {"x": 1537.28, "y": 66.64, "z": 18.78}, {"x": 1537.41, "y": 65.96, "z": 18.81}, {"x": 1537.55, "y": 65.32, "z": 18.85}, {"x": 1538.03, "y": 64.32, "z": 18.93}, {"x": 1538.44, "y": 63.74, "z": 18.98}, {"x": 1539.29, "y": 62.81, "z": 19.04}, {"x": 1554.79, "y": 54.37, "z": 20.07}, {"x": 1548.0, "y": 38.33, "z": 20.45}, {"x": 1538.4, "y": 43.35, "z": 19.8}, {"x": 1534.72, "y": 45.29, "z": 19.56}, {"x": 1533.52, "y": 45.78, "z": 19.55}, {"x": 1532.61, "y": 46.17, "z": 19.5}, {"x": 1532.03, "y": 46.3, "z": 19.5}, {"x": 1531.46, "y": 46.44, "z": 19.47}, {"x": 1530.79, "y": 46.55, "z": 19.43}, {"x": 1530.39, "y": 46.52, "z": 19.41}, {"x": 1530.0, "y": 46.5, "z": 19.39}, {"x": 1529.44, "y": 46.41, "z": 19.35}, {"x": 1528.91, "y": 46.32, "z": 19.32}, {"x": 1528.3, "y": 46.13, "z": 19.3}, {"x": 1527.85, "y": 45.91, "z": 19.29}, {"x": 1527.46, "y": 45.71, "z": 19.31}, {"x": 1527.07, "y": 45.49, "z": 19.33}, {"x": 1526.71, "y": 45.24, "z": 19.33}, {"x": 1526.34, "y": 44.9, "z": 19.32}, {"x": 1525.94, "y": 44.49, "z": 19.35}, {"x": 1525.45, "y": 44.0, "z": 19.33}, {"x": 1524.99, "y": 43.34, "z": 19.34}, {"x": 1524.02, "y": 41.32, "z": 19.37}, {"x": 1522.01, "y": 37.43, "z": 19.41}, {"x": 1517.93, "y": 28.67, "z": 19.59}, {"x": 1513.79, "y": 20.36, "z": 19.81}, {"x": 1506.1, "y": 4.73, "z": 20.13}, {"x": 1497.35, "y": -12.74, "z": 20.54}, {"x": 1491.36, "y": -10.12, "z": 20.53}, {"x": 1496.82, "y": 0.73, "z": 20.33}, {"x": 1496.88, "y": 1.17, "z": 20.32}, {"x": 1496.6, "y": 1.52, "z": 20.33}, {"x": 1496.25, "y": 1.91, "z": 20.33}, {"x": 1495.47, "y": 2.38, "z": 20.35}, {"x": 1494.89, "y": 2.67, "z": 20.35}, {"x": 1493.97, "y": 2.7, "z": 20.32}, {"x": 1493.3, "y": 2.28, "z": 20.32}, {"x": 1490.34, "y": -3.51, "z": 20.44}, {"x": 1487.69, "y": -8.53, "z": 20.53}, {"x": 1482.52, "y": -6.35, "z": 20.43}, {"x": 1499.52, "y": 28.31, "z": 19.77}, {"x": 1505.12, "y": 39.81, "z": 19.53}, {"x": 1509.43, "y": 48.42, "z": 19.23}, {"x": 1511.57, "y": 52.64, "z": 19.01}, {"x": 1511.76, "y": 53.37, "z": 18.96}, {"x": 1511.8, "y": 54.05, "z": 18.91}, {"x": 1511.77, "y": 54.71, "z": 18.89}, {"x": 1511.66, "y": 55.28, "z": 18.86}, {"x": 1511.46, "y": 55.78, "z": 18.84}, {"x": 1511.19, "y": 56.28, "z": 18.81}, {"x": 1510.9, "y": 56.63, "z": 18.79}, {"x": 1510.64, "y": 56.9, "z": 18.79}, {"x": 1510.34, "y": 57.15, "z": 18.78}, {"x": 1510.0, "y": 57.38, "z": 18.79}, {"x": 1509.51, "y": 57.62, "z": 18.74}, {"x": 1456.51, "y": 82.9, "z": 16.15}, {"x": 1454.57, "y": 83.71, "z": 16.06}, {"x": 1453.28, "y": 83.5, "z": 16.01}, {"x": 1452.73, "y": 83.26, "z": 16.0}, {"x": 1452.3, "y": 83.03, "z": 16.0}, {"x": 1451.83, "y": 82.51, "z": 16.02}, {"x": 1451.51, "y": 82.07, "z": 16.03}, {"x": 1451.29, "y": 81.75, "z": 16.04}, {"x": 1451.02, "y": 81.12, "z": 16.06}, {"x": 1425.1, "y": 30.0, "z": 17.41}, {"x": 1415.07, "y": 30.0, "z": 17.46}, {"x": 1434.92, "y": 69.4, "z": 16.48}, {"x": 1443.29, "y": 85.7, "z": 15.93}, {"x": 1443.6, "y": 86.66, "z": 15.9}, {"x": 1443.67, "y": 87.42, "z": 15.86}, {"x": 1443.57, "y": 88.15, "z": 15.82}, {"x": 1443.44, "y": 88.53, "z": 15.81}, {"x": 1443.16, "y": 89.01, "z": 15.79}, {"x": 1442.79, "y": 89.36, "z": 15.79}, {"x": 1441.82, "y": 89.99, "z": 15.77}, {"x": 1408.35, "y": 105.76, "z": 15.38}, {"x": 1407.8, "y": 106.01, "z": 15.35}, {"x": 1407.44, "y": 106.1, "z": 15.35}, {"x": 1407.07, "y": 106.08, "z": 15.35}, {"x": 1406.74, "y": 106.01, "z": 15.35}, {"x": 1406.37, "y": 105.86, "z": 15.31}, {"x": 1406.12, "y": 105.65, "z": 15.3}, {"x": 1405.83, "y": 105.25, "z": 15.31}, {"x": 1404.93, "y": 103.35, "z": 15.43}, {"x": 1403.72, "y": 101.0, "z": 15.49}, {"x": 1393.51, "y": 80.9, "z": 15.98}, {"x": 1390.24, "y": 82.77, "z": 15.71}, {"x": 1399.57, "y": 102.04, "z": 15.4}, {"x": 1400.08, "y": 103.05, "z": 15.42}, {"x": 1402.4, "y": 107.9, "z": 15.23}, {"x": 1402.39, "y": 108.26, "z": 15.22}, {"x": 1402.17, "y": 108.54, "z": 15.2}, {"x": 1401.71, "y": 108.94, "z": 15.18}, {"x": 1401.14, "y": 109.22, "z": 15.22}, {"x": 1399.6, "y": 109.91, "z": 15.16}, {"x": 1386.67, "y": 116.14, "z": 14.88}, {"x": 1372.47, "y": 123.03, "z": 14.6}, {"x": 1371.56, "y": 123.45, "z": 14.62}, {"x": 1370.92, "y": 123.61, "z": 14.63}, {"x": 1370.26, "y": 123.63, "z": 14.63}, {"x": 1369.72, "y": 123.58, "z": 14.64}, {"x": 1369.18, "y": 123.47, "z": 14.64}, {"x": 1368.55, "y": 123.25, "z": 14.65}, {"x": 1368.0, "y": 122.96, "z": 14.66}, {"x": 1367.38, "y": 122.44, "z": 14.67}, {"x": 1366.87, "y": 121.82, "z": 14.68}, {"x": 1366.42, "y": 121.08, "z": 14.69}, {"x": 1365.98, "y": 120.16, "z": 14.71}, {"x": 1365.51, "y": 119.26, "z": 14.74}, {"x": 1361.47, "y": 110.84, "z": 14.94}, {"x": 1350.0, "y": 87.72, "z": 15.36}, {"x": 1350.0, "y": 111.99, "z": 14.89}, {"x": 1356.45, "y": 124.92, "z": 14.57}, {"x": 1356.99, "y": 126.05, "z": 14.5}, {"x": 1357.22, "y": 126.82, "z": 14.5}, {"x": 1357.13, "y": 127.81, "z": 14.46}, {"x": 1357.03, "y": 128.58, "z": 14.44}, {"x": 1356.85, "y": 129.17, "z": 14.44}, {"x": 1356.55, "y": 129.71, "z": 14.44}, {"x": 1356.11, "y": 130.34, "z": 14.43}, {"x": 1355.55, "y": 130.98, "z": 14.42}, {"x": 1354.71, "y": 131.53, "z": 14.4}, {"x": 1350.0, "y": 133.71, "z": 14.28}, {"x": 1350.0, "y": 145.18, "z": 13.96}, {"x": 1359.81, "y": 140.76, "z": 14.21}, {"x": 1361.08, "y": 140.25, "z": 14.23}, {"x": 1362.06, "y": 139.98, "z": 14.23}, {"x": 1363.03, "y": 140.06, "z": 14.23}, {"x": 1363.82, "y": 140.35, "z": 14.23}, {"x": 1364.34, "y": 140.91, "z": 14.22}, {"x": 1365.3, "y": 142.88, "z": 14.19}, {"x": 1368.79, "y": 150.0, "z": 13.93}, {"x": 1380.87, "y": 150.0, "z": 13.95}, {"x": 1378.85, "y": 146.27, "z": 14.08}, {"x": 1374.85, "y": 137.97, "z": 14.3}, {"x": 1374.6, "y": 137.14, "z": 14.31}, {"x": 1374.4, "y": 136.22, "z": 14.31}, {"x": 1374.49, "y": 135.45, "z": 14.32}, {"x": 1374.77, "y": 134.69, "z": 14.34}, {"x": 1375.32, "y": 133.93, "z": 14.38}, {"x": 1376.19, "y": 133.1, "z": 14.44}, {"x": 1377.39, "y": 132.42, "z": 14.48}, {"x": 1391.34, "y": 125.83, "z": 14.8}, {"x": 1400.8, "y": 121.3, "z": 15.0}, {"x": 1412.93, "y": 115.5, "z": 15.25}, {"x": 1421.65, "y": 111.39, "z": 15.35}, {"x": 1427.74, "y": 108.48, "z": 15.44}, {"x": 1437.6, "y": 103.68, "z": 15.52}, {"x": 1443.4, "y": 101.12, "z": 15.57}, {"x": 1444.96, "y": 100.57, "z": 15.61}, {"x": 1446.02, "y": 100.29, "z": 15.61}, {"x": 1446.65, "y": 100.16, "z": 15.62}, {"x": 1447.33, "y": 100.1, "z": 15.62}, {"x": 1447.87, "y": 100.14, "z": 15.62}, {"x": 1448.4, "y": 100.16, "z": 15.62}, {"x": 1448.75, "y": 100.16, "z": 15.62}, {"x": 1449.27, "y": 100.35, "z": 15.62}, {"x": 1449.71, "y": 100.47, "z": 15.62}, {"x": 1450.16, "y": 100.65, "z": 15.61}, {"x": 1450.44, "y": 100.83, "z": 15.61}, {"x": 1450.77, "y": 101.09, "z": 15.61}, {"x": 1451.04, "y": 101.38, "z": 15.61}, {"x": 1451.32, "y": 101.73, "z": 15.6}, {"x": 1451.55, "y": 102.18, "z": 15.6}, {"x": 1460.69, "y": 120.0, "z": 15.1}, {"x": 1470.79, "y": 120.0, "z": 14.92}, {"x": 1463.17, "y": 104.98, "z": 15.31}, {"x": 1462.3, "y": 103.42, "z": 15.41}, {"x": 1461.97, "y": 102.67, "z": 15.43}, {"x": 1461.83, "y": 102.3, "z": 15.46}, {"x": 1461.7, "y": 101.83, "z": 15.48}, {"x": 1461.68, "y": 101.34, "z": 15.48}, {"x": 1461.68, "y": 100.86, "z": 15.49}, {"x": 1461.72, "y": 100.47, "z": 15.5}, {"x": 1461.85, "y": 100.02, "z": 15.5}, {"x": 1462.01, "y": 99.69, "z": 15.52}, {"x": 1462.3, "y": 99.28, "z": 15.58}, {"x": 1462.69, "y": 98.92, "z": 15.65}, {"x": 1463.18, "y": 98.53, "z": 15.69}, {"x": 1463.68, "y": 98.24, "z": 15.74}, {"x": 1465.22, "y": 97.41, "z": 15.82}, {"x": 1486.35, "y": 87.37, "z": 16.89}, {"x": 1505.05, "y": 78.47, "z": 18.04}, {"x": 1514.38, "y": 73.95, "z": 18.49}, {"x": 1515.31, "y": 73.45, "z": 18.52}, {"x": 1516.22, "y": 73.05, "z": 18.51}, {"x": 1517.0, "y": 72.73, "z": 18.61}, {"x": 1517.84, "y": 72.43, "z": 18.62}, {"x": 1518.43, "y": 72.33, "z": 18.62}, {"x": 1519.1, "y": 72.3, "z": 18.63}, {"x": 1519.75, "y": 72.47, "z": 18.64}, {"x": 1520.65, "y": 72.94, "z": 18.66}, {"x": 1521.35, "y": 73.5, "z": 18.68}, {"x": 1522.07, "y": 74.41, "z": 18.71}, {"x": 1531.69, "y": 93.82, "z": 18.13}, {"x": 1544.61, "y": 120.0, "z": 17.14}], "id": 1413627}}} \ No newline at end of file diff --git a/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/sensors/lidar/315973157959879000.feather b/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/sensors/lidar/315973157959879000.feather new file mode 100644 index 00000000..b716a892 Binary files /dev/null and b/tests/unit/test_data/sensor_dataset_logs/av2/sensor/val/adcf7d18-0510-35b0-a2fa-b4cea13a6d76/sensors/lidar/315973157959879000.feather differ diff --git a/tests/test_data/sensor_dataset_logs/dummy/calibration/egovehicle_SE3_sensor.feather b/tests/unit/test_data/sensor_dataset_logs/dummy/calibration/egovehicle_SE3_sensor.feather similarity index 100% rename from tests/test_data/sensor_dataset_logs/dummy/calibration/egovehicle_SE3_sensor.feather rename to tests/unit/test_data/sensor_dataset_logs/dummy/calibration/egovehicle_SE3_sensor.feather diff --git a/tests/test_data/sensor_dataset_logs/dummy/sensors/lidar/315968663259918000.feather b/tests/unit/test_data/sensor_dataset_logs/dummy/sensors/lidar/315968663259918000.feather similarity index 100% rename from tests/test_data/sensor_dataset_logs/dummy/sensors/lidar/315968663259918000.feather rename to tests/unit/test_data/sensor_dataset_logs/dummy/sensors/lidar/315968663259918000.feather diff --git a/tests/test_data/sensor_dataset_logs/test_log/calibration/egovehicle_SE3_sensor.feather b/tests/unit/test_data/sensor_dataset_logs/test_log/calibration/egovehicle_SE3_sensor.feather similarity index 100% rename from tests/test_data/sensor_dataset_logs/test_log/calibration/egovehicle_SE3_sensor.feather rename to tests/unit/test_data/sensor_dataset_logs/test_log/calibration/egovehicle_SE3_sensor.feather diff --git a/tests/test_data/sensor_dataset_logs/test_log/calibration/intrinsics.feather b/tests/unit/test_data/sensor_dataset_logs/test_log/calibration/intrinsics.feather similarity index 100% rename from tests/test_data/sensor_dataset_logs/test_log/calibration/intrinsics.feather rename to tests/unit/test_data/sensor_dataset_logs/test_log/calibration/intrinsics.feather diff --git a/tests/test_data/static_maps/dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076/gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076___img_Sim2_city.json b/tests/unit/test_data/static_maps/dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076/gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076___img_Sim2_city.json similarity index 100% rename from tests/test_data/static_maps/dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076/gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076___img_Sim2_city.json rename to tests/unit/test_data/static_maps/dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076/gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076___img_Sim2_city.json diff --git a/tests/test_data/static_maps/dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076/gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076_ground_height_surface____MIA.npy b/tests/unit/test_data/static_maps/dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076/gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076_ground_height_surface____MIA.npy similarity index 100% rename from tests/test_data/static_maps/dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076/gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076_ground_height_surface____MIA.npy rename to tests/unit/test_data/static_maps/dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076/gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076_ground_height_surface____MIA.npy diff --git a/tests/test_data/static_maps/dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076/log_map_archive_dummy_log_map_v2_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076.json b/tests/unit/test_data/static_maps/dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076/log_map_archive_dummy_log_map_v2_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076.json similarity index 100% rename from tests/test_data/static_maps/dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076/log_map_archive_dummy_log_map_v2_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076.json rename to tests/unit/test_data/static_maps/dummy_log_map_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076/log_map_archive_dummy_log_map_v2_gs1B8ZCv7DMi8cMt5aN5rSYjQidJXvGP__2020-07-21-Z1F0076.json diff --git a/tests/unit/torch/__init__.py b/tests/unit/torch/__init__.py new file mode 100644 index 00000000..e3de7e3e --- /dev/null +++ b/tests/unit/torch/__init__.py @@ -0,0 +1 @@ +"""Torch tests sub-package.""" diff --git a/tests/unit/torch/data_loaders/__init__.py b/tests/unit/torch/data_loaders/__init__.py new file mode 100644 index 00000000..dc883d64 --- /dev/null +++ b/tests/unit/torch/data_loaders/__init__.py @@ -0,0 +1 @@ +"""PyTorch Data-loaders tests sub-package.""" diff --git a/tests/unit/torch/data_loaders/test_detection_dataloader.py b/tests/unit/torch/data_loaders/test_detection_dataloader.py new file mode 100644 index 00000000..a7cf547b --- /dev/null +++ b/tests/unit/torch/data_loaders/test_detection_dataloader.py @@ -0,0 +1,22 @@ +"""Unit tests for PyTorch Detection Dataset sub-module.""" + +from pathlib import Path +from typing import Final + +from av2.torch.data_loaders.detection import DetectionDataLoader + +TEST_DATA_DIR: Final = ( + Path(__file__).parent.parent.parent.resolve() / "test_data" / "sensor_dataset_logs" +) + + +def test_build_data_loader() -> None: + """Test building the PyTorch Detection DataLoader.""" + root_dir = TEST_DATA_DIR + dataset_name = "av2" + split_name = "val" + data_loader = DetectionDataLoader( + root_dir=root_dir, dataset_name=dataset_name, split_name=split_name + ) + for datum in data_loader: + assert datum is not None diff --git a/tests/unit/torch/data_loaders/test_scene_flow_dataloader.py b/tests/unit/torch/data_loaders/test_scene_flow_dataloader.py new file mode 100644 index 00000000..ed481b25 --- /dev/null +++ b/tests/unit/torch/data_loaders/test_scene_flow_dataloader.py @@ -0,0 +1,85 @@ +"""Unit tests on scene_flow data-loader.""" + +from pathlib import Path + +import numpy as np +import pandas as pd + +import av2.torch.data_loaders.scene_flow +from av2.evaluation.scene_flow.utils import compute_eval_point_mask, get_eval_subset +from av2.torch.structures.flow import Flow +from av2.torch.structures.sweep import Sweep +from av2.utils.typing import NDArrayBool + +_TEST_DATA_ROOT = Path(__file__).resolve().parent.parent.parent + + +def test_scene_flow_dataloader() -> None: + """Test loading a single pair with flow annotations. + + The computed flow should check the visually confirmed labels in flow_labels.feather. + """ + dl_test = av2.torch.data_loaders.scene_flow.SceneFlowDataloader( + _TEST_DATA_ROOT, "test_data", "test" + ) + sweep_0, sweep_1, ego, not_flow = dl_test[0] + assert not_flow is None + rust_sweep = dl_test._backend.get(0) + sweep_0 = Sweep.from_rust(rust_sweep) + assert sweep_0.cuboids is None + + failed = False + try: + compute_eval_point_mask((sweep_0, sweep_1, ego, not_flow)) + except ValueError: + failed = True + assert failed + + failed = False + try: + Flow.from_sweep_pair((sweep_0, sweep_1)) + except ValueError: + failed = True + assert failed + + data_loader = av2.torch.data_loaders.scene_flow.SceneFlowDataloader( + _TEST_DATA_ROOT, "test_data", "val" + ) + assert len(data_loader) == 1 + assert data_loader.get_log_id(0) == "7fab2350-7eaf-3b7e-a39d-6937a4c1bede" + + for datum in data_loader: + sweep_0, sweep_1, ego, maybe_flow = datum + + assert maybe_flow is not None + flow: Flow = maybe_flow + assert len(flow) == len(sweep_0.lidar.as_tensor()) + + log_dir = ( + _TEST_DATA_ROOT / "test_data/sensor/val/7fab2350-7eaf-3b7e-a39d-6937a4c1bede" + ) + flow_labels = pd.read_feather(log_dir / "flow_labels.feather") + + FLOW_COLS = ["flow_tx_m", "flow_ty_m", "flow_tz_m"] + err = np.abs(flow.flow.numpy() - flow_labels[FLOW_COLS].to_numpy()).sum(-1) + max_err_ind = np.argmax(err) + flow_err_val = flow.flow.numpy()[max_err_ind] + label_err_val = flow_labels[FLOW_COLS].to_numpy()[max_err_ind] + assert np.allclose( + flow.flow.numpy(), flow_labels[FLOW_COLS].to_numpy(), atol=1e-3 + ), f"max-diff {err[max_err_ind]} ind: {max_err_ind} flow: {flow_err_val} label: {label_err_val}" + assert np.allclose(flow.category_indices.numpy(), flow_labels.classes.to_numpy()) + assert np.allclose(flow.is_dynamic.numpy(), flow_labels.dynamic.to_numpy()) + assert sweep_0.is_ground is not None + ground_match: NDArrayBool = ( + sweep_0.is_ground.numpy() == flow_labels.is_ground_0.to_numpy() + ) + assert np.logical_not(ground_match).sum() < 10 + + gt_ego = np.load(log_dir / "ego_motion.npz") + + assert np.allclose(ego.matrix().numpy(), gt_ego["ego_motion"], atol=1e-3) + + eval_inds = get_eval_subset(data_loader) + assert len(eval_inds) == 1 + assert eval_inds[0] == 0 diff --git a/tests/unit/torch/structures/__init__.py b/tests/unit/torch/structures/__init__.py new file mode 100644 index 00000000..80a1e8ca --- /dev/null +++ b/tests/unit/torch/structures/__init__.py @@ -0,0 +1 @@ +"""PyTorch structures tests.""" diff --git a/tests/unit/torch/structures/test_cuboids.py b/tests/unit/torch/structures/test_cuboids.py new file mode 100644 index 00000000..ae2ed18c --- /dev/null +++ b/tests/unit/torch/structures/test_cuboids.py @@ -0,0 +1,46 @@ +"""Unit tests for PyTorch Cuboids sub-module.""" + +from pathlib import Path +from typing import Final, List + +import numpy as np +import pandas as pd +import torch +from kornia.geometry.conversions import euler_from_quaternion +from torch.testing._comparison import assert_close + +from av2.torch import XYZLWH_QWXYZ_COLUMNS +from av2.torch.structures.cuboids import CuboidMode, Cuboids + +TEST_DATA_DIR: Final = Path(__file__).parent.parent.parent.resolve() / "test_data" +SAMPLE_LOG_DIR: Final = ( + TEST_DATA_DIR / "sensor_dataset_logs" / "adcf7d18-0510-35b0-a2fa-b4cea13a6d76" +) + + +def test_build_cuboids() -> None: + """Test building the Cuboids structure.""" + annotations_path = SAMPLE_LOG_DIR / "annotations.feather" + annotations_frame = pd.read_feather(annotations_path) + cuboids_npy = ( + annotations_frame[list(XYZLWH_QWXYZ_COLUMNS)].to_numpy().astype(np.float32) + ) + + cuboids = Cuboids(annotations_frame) + cuboids_xyzlwht = cuboids.as_tensor() + + cuboids_xyzlwh_qwxyz = torch.as_tensor(cuboids_npy) + assert_close(cuboids_xyzlwht[:, :6], cuboids_xyzlwh_qwxyz[:, :6]) + + w, x, y, z = cuboids_xyzlwh_qwxyz[:, 6:10].t() + _, _, yaw = euler_from_quaternion(w, x, y, z) + assert_close(cuboids_xyzlwht[:, 6], yaw) + assert_close( + cuboids.as_tensor(cuboid_mode=CuboidMode.XYZLWH_QWXYZ), cuboids_xyzlwh_qwxyz + ) + + track_uuid_expected: List[str] = annotations_frame["track_uuid"].to_list() + assert cuboids.track_uuid == track_uuid_expected + + category_expected: List[str] = annotations_frame["category"].to_list() + assert cuboids.category == category_expected diff --git a/tests/unit/torch/structures/test_lidar.py b/tests/unit/torch/structures/test_lidar.py new file mode 100644 index 00000000..2e7604a9 --- /dev/null +++ b/tests/unit/torch/structures/test_lidar.py @@ -0,0 +1,39 @@ +"""Unit tests for PyTorch Lidar sub-module.""" + +from pathlib import Path +from typing import Final + +import numpy as np +import pandas as pd +import torch +from torch.testing._comparison import assert_close + +from av2.torch import LIDAR_COLUMNS +from av2.torch.structures.lidar import Lidar + +TEST_DATA_DIR: Final = Path(__file__).parent.parent.parent.resolve() / "test_data" +SAMPLE_LOG_DIR: Final = ( + TEST_DATA_DIR / "sensor_dataset_logs" / "adcf7d18-0510-35b0-a2fa-b4cea13a6d76" +) + + +def test_build_lidar() -> None: + """Test building a Lidar structure.""" + lidar_paths = sorted((SAMPLE_LOG_DIR / "sensors" / "lidar").glob("*.feather")) + lidar_path = lidar_paths[0] + frame = pd.read_feather(lidar_path) + lidar_tensor = torch.as_tensor( + frame[list(LIDAR_COLUMNS)].to_numpy().astype(np.float32) + ) + + lidar = Lidar(frame) + assert_close(lidar.as_tensor(), lidar_tensor) + assert_close( + lidar.as_tensor( + columns=( + "y", + "z", + ) + ), + lidar_tensor[:, 1:3], + ) diff --git a/tests/unit/torch/structures/test_sweep.py b/tests/unit/torch/structures/test_sweep.py new file mode 100644 index 00000000..758f7c48 --- /dev/null +++ b/tests/unit/torch/structures/test_sweep.py @@ -0,0 +1,38 @@ +"""Unit tests for PyTorch Cuboids sub-module.""" + +from pathlib import Path +from typing import Final + +import pandas as pd + +from av2.torch.structures.cuboids import Cuboids +from av2.torch.structures.lidar import Lidar +from av2.torch.structures.sweep import Sweep +from av2.torch.structures.utils import SE3_from_frame + +TEST_DATA_DIR: Final = Path(__file__).parent.parent.parent.resolve() / "test_data" +SAMPLE_LOG_DIR: Final = ( + TEST_DATA_DIR / "sensor_dataset_logs" / "adcf7d18-0510-35b0-a2fa-b4cea13a6d76" +) + + +def test_build_sweep() -> None: + """Test building the Sweep structure.""" + annotations_path = SAMPLE_LOG_DIR / "annotations.feather" + annotations_frame = pd.read_feather(annotations_path) + cuboids = Cuboids(annotations_frame) + + lidar_paths = sorted((SAMPLE_LOG_DIR / "sensors" / "lidar").glob("*.feather")) + lidar_path = lidar_paths[0] + lidar_frame = pd.read_feather(lidar_path) + lidar = Lidar(lidar_frame) + + city_pose_path = SAMPLE_LOG_DIR / "city_SE3_egovehicle.feather" + city_pose_frame = pd.read_feather(city_pose_path) + city_SE3_ego = SE3_from_frame(city_pose_frame) + + sweep_uuid = annotations_path.parent.stem, int(lidar_path.stem) + sweep = Sweep( + city_SE3_ego=city_SE3_ego, lidar=lidar, sweep_uuid=sweep_uuid, cuboids=cuboids + ) + assert sweep is not None diff --git a/tests/unit/torch/structures/test_utils.py b/tests/unit/torch/structures/test_utils.py new file mode 100644 index 00000000..43a8712c --- /dev/null +++ b/tests/unit/torch/structures/test_utils.py @@ -0,0 +1,66 @@ +"""Unit test for PyTorch structure utilities.""" + +import numpy as np +import pandas as pd +import torch +from kornia.geometry.liegroup import Se3, So3 +from kornia.geometry.quaternion import Quaternion +from torch.testing._comparison import assert_close + +from av2.torch import QWXYZ_COLUMNS, TRANSLATION_COLUMNS +from av2.torch.structures.utils import SE3_from_frame, tensor_from_frame + + +def _build_dummy_frame() -> pd.DataFrame: + """Build a dummy data-frame.""" + return pd.DataFrame( + { + "tx_m": [0.0], + "ty_m": [1.0], + "tz_m": [0.0], + "qw": [1.0], + "qx": [0.0], + "qy": [0.0], + "qz": [0.0], + }, + dtype=np.float32, + ) + + +def test_tensor_from_frame() -> None: + """Test converting a data-frame into a tensor.""" + frame = _build_dummy_frame() + tensor = tensor_from_frame(frame, columns=["qw", "qx", "qy", "qz"]) + + tensor_expected = torch.as_tensor( + [ + [ + frame.loc[0, "qw"], + frame.loc[0, "qx"], + frame.loc[0, "qy"], + frame.loc[0, "qz"], + ] + ] + ) + assert_close(tensor, tensor_expected) + + +def test_SE3_from_frame() -> None: + """Test converting a data-frame into an SE(3) object.""" + frame = _build_dummy_frame() + + quat_wxyz_tensor = torch.as_tensor( + frame[list(QWXYZ_COLUMNS)].to_numpy().astype(np.float32) + ) + translation = torch.as_tensor( + frame[list(TRANSLATION_COLUMNS)].to_numpy().astype(np.float32) + ) + quat_wxyz = Quaternion(quat_wxyz_tensor) + rotation = So3(quat_wxyz) + city_SE3_ego_expected = Se3(rotation, translation) + city_SE3_ego = SE3_from_frame(frame) + + assert_close(city_SE3_ego.translation, city_SE3_ego_expected.translation) + assert_close( + city_SE3_ego.rotation.matrix(), city_SE3_ego_expected.rotation.matrix() + ) diff --git a/tests/utils/test_dense_grid_interpolation.py b/tests/unit/utils/test_dense_grid_interpolation.py similarity index 81% rename from tests/utils/test_dense_grid_interpolation.py rename to tests/unit/utils/test_dense_grid_interpolation.py index 3b36023a..dc2148dd 100644 --- a/tests/utils/test_dense_grid_interpolation.py +++ b/tests/unit/utils/test_dense_grid_interpolation.py @@ -25,7 +25,12 @@ def test_interp_dense_grid_from_sparse_insufficient_points_simplex() -> None: grid_w = 10 bev_img_interp = dense_grid_interpolation.interp_dense_grid_from_sparse( - grid_img=bev_img, points=points, values=rgb_values, grid_h=grid_h, grid_w=grid_w, interp_method="linear" + grid_img=bev_img, + points=points, + values=rgb_values, + grid_h=grid_h, + grid_w=grid_w, + interp_method="linear", ) assert np.allclose(bev_img_interp, np.zeros((10, 10, 3), dtype=np.uint8)) @@ -44,12 +49,19 @@ def test_interp_dense_grid_from_sparse_byte() -> None: # provided as (x,y) tuples points: NDArrayInt = np.array([[0, 0], [0, 3], [3, 3], [3, 0]]) - rgb_values: NDArrayByte = np.array([RED_RGB, GREEN_RGB, BLUE_RGB, RED_RGB], dtype=np.uint8) + rgb_values: NDArrayByte = np.array( + [RED_RGB, GREEN_RGB, BLUE_RGB, RED_RGB], dtype=np.uint8 + ) grid_h = 4 grid_w = 4 bev_img_interp = dense_grid_interpolation.interp_dense_grid_from_sparse( - grid_img=bev_img, points=points, values=rgb_values, grid_h=grid_h, grid_w=grid_w, interp_method="linear" + grid_img=bev_img, + points=points, + values=rgb_values, + grid_h=grid_h, + grid_w=grid_w, + interp_method="linear", ) assert bev_img_interp.dtype == np.dtype(np.uint8) @@ -77,12 +89,19 @@ def test_interp_dense_grid_from_sparse_float() -> None: # provided as (x,y) tuples points: NDArrayInt = np.array([[0, 0], [0, 3], [3, 3], [3, 0]], dtype=int) - rgb_values: NDArrayFloat = np.array([RED_RGB, GREEN_RGB, BLUE_RGB, RED_RGB], dtype=float) + rgb_values: NDArrayFloat = np.array( + [RED_RGB, GREEN_RGB, BLUE_RGB, RED_RGB], dtype=float + ) grid_h = 4 grid_w = 4 bev_img_interp = dense_grid_interpolation.interp_dense_grid_from_sparse( - grid_img=bev_img, points=points, values=rgb_values, grid_h=grid_h, grid_w=grid_w, interp_method="linear" + grid_img=bev_img, + points=points, + values=rgb_values, + grid_h=grid_h, + grid_w=grid_w, + interp_method="linear", ) assert bev_img_interp.dtype == np.dtype(np.float64) diff --git a/tests/utils/test_depth_map_utils.py b/tests/unit/utils/test_depth_map_utils.py similarity index 67% rename from tests/utils/test_depth_map_utils.py rename to tests/unit/utils/test_depth_map_utils.py index d0a0390a..2f2e6f91 100644 --- a/tests/utils/test_depth_map_utils.py +++ b/tests/unit/utils/test_depth_map_utils.py @@ -18,14 +18,20 @@ def test_vis_depth_map() -> None: img_rgb: NDArrayByte = np.zeros((H, W, 3), dtype=np.uint8) img_rgb[:, :, 0] = 255 # channels will be (255,0,0) for red. - depth_map: NDArrayFloat = np.arange(H * W).reshape(H, W).astype(np.float32) / (H * W) * 255 + depth_map: NDArrayFloat = ( + np.arange(H * W).reshape(H, W).astype(np.float32) / (H * W) * 255 + ) - depth_map_utils.vis_depth_map(img_rgb=img_rgb, depth_map=depth_map, interp_depth_map=True) + depth_map_utils.vis_depth_map( + img_rgb=img_rgb, depth_map=depth_map, interp_depth_map=True + ) if visualize: plt.show() plt.close("all") - depth_map_utils.vis_depth_map(img_rgb=img_rgb, depth_map=depth_map, interp_depth_map=False) + depth_map_utils.vis_depth_map( + img_rgb=img_rgb, depth_map=depth_map, interp_depth_map=False + ) if visualize: plt.show() plt.close("all") diff --git a/tests/utils/test_dilate_utils_unit.py b/tests/unit/utils/test_dilate_utils_unit.py similarity index 100% rename from tests/utils/test_dilate_utils_unit.py rename to tests/unit/utils/test_dilate_utils_unit.py diff --git a/tests/utils/test_infinity_norm_utils.py b/tests/unit/utils/test_infinity_norm_utils.py similarity index 85% rename from tests/utils/test_infinity_norm_utils.py rename to tests/unit/utils/test_infinity_norm_utils.py index 369575f0..e2497fbd 100644 --- a/tests/utils/test_infinity_norm_utils.py +++ b/tests/unit/utils/test_infinity_norm_utils.py @@ -18,7 +18,9 @@ def test_has_pts_in_infinity_norm_radius1() -> None: [5.1, 5.1] ]) # fmt: on - within = infinity_norm_utils.has_pts_in_infinity_norm_radius(pts, window_center=np.zeros(2), window_sz=5) + within = infinity_norm_utils.has_pts_in_infinity_norm_radius( + pts, window_center=np.zeros(2), window_sz=5 + ) assert not within @@ -32,7 +34,9 @@ def test_has_pts_in_infinity_norm_radius2() -> None: [5.1, 5.1] ]) # fmt: on - within = infinity_norm_utils.has_pts_in_infinity_norm_radius(pts, window_center=np.zeros(2), window_sz=5) + within = infinity_norm_utils.has_pts_in_infinity_norm_radius( + pts, window_center=np.zeros(2), window_sz=5 + ) assert within @@ -46,12 +50,16 @@ def test_has_pts_in_infinity_norm_radius3() -> None: [4.9, 4.9] ]) # fmt: on - within = infinity_norm_utils.has_pts_in_infinity_norm_radius(pts, window_center=np.zeros(2), window_sz=5) + within = infinity_norm_utils.has_pts_in_infinity_norm_radius( + pts, window_center=np.zeros(2), window_sz=5 + ) assert within def test_has_pts_in_infinity_norm_radius4() -> None: """All pts within radius.""" pts: NDArrayFloat = np.array([[4.9, 4.9]]) - within = infinity_norm_utils.has_pts_in_infinity_norm_radius(pts, window_center=np.zeros(2), window_sz=5) + within = infinity_norm_utils.has_pts_in_infinity_norm_radius( + pts, window_center=np.zeros(2), window_sz=5 + ) assert within diff --git a/tests/utils/test_io.py b/tests/unit/utils/test_io.py similarity index 79% rename from tests/utils/test_io.py rename to tests/unit/utils/test_io.py index be66b96f..ecfd8f3d 100644 --- a/tests/utils/test_io.py +++ b/tests/unit/utils/test_io.py @@ -12,7 +12,13 @@ def test_read_feather(test_data_root_dir: Path) -> None: """Read an Apache Feather file.""" - feather_path = test_data_root_dir / "sensor_dataset_logs" / "test_log" / "calibration" / "intrinsics.feather" + feather_path = ( + test_data_root_dir + / "sensor_dataset_logs" + / "test_log" + / "calibration" + / "intrinsics.feather" + ) feather_file = read_feather(feather_path) assert feather_file is not None @@ -31,7 +37,12 @@ def test_read_ego_SE3_sensor(test_data_root_dir: Path) -> None: def test_read_lidar_sweep() -> None: """Read 3d point coordinates from a LiDAR sweep file from an example log.""" log_id = "adcf7d18-0510-35b0-a2fa-b4cea13a6d76" - EXAMPLE_LOG_DATA_ROOT = Path(__file__).resolve().parent.parent / "test_data" / "sensor_dataset_logs" / log_id + EXAMPLE_LOG_DATA_ROOT = ( + Path(__file__).resolve().parent.parent + / "test_data" + / "sensor_dataset_logs" + / log_id + ) fpath = EXAMPLE_LOG_DATA_ROOT / "sensors" / "lidar" / "315973157959879000.feather" arr = io_utils.read_lidar_sweep(fpath, attrib_spec="xyz") diff --git a/tests/utils/test_mesh_grid.py b/tests/unit/utils/test_mesh_grid.py similarity index 86% rename from tests/utils/test_mesh_grid.py rename to tests/unit/utils/test_mesh_grid.py index 731aa881..2d01e09d 100644 --- a/tests/utils/test_mesh_grid.py +++ b/tests/unit/utils/test_mesh_grid.py @@ -18,7 +18,9 @@ def test_get_mesh_grid_as_point_cloud_3x3square() -> None: max_y = 4 # integer, maximum y-coordinate of 2D grid # return pts, a Numpy array of shape (N,2) - pts = mesh_grid_utils.get_mesh_grid_as_point_cloud(min_x, max_x, min_y, max_y, downsample_factor=1.0) + pts = mesh_grid_utils.get_mesh_grid_as_point_cloud( + min_x, max_x, min_y, max_y, downsample_factor=1.0 + ) assert pts.shape == (9, 2) gt_pts: NDArrayFloat = np.array( @@ -46,7 +48,9 @@ def test_get_mesh_grid_as_point_cloud_3x2rect() -> None: max_y = 3 # integer, maximum y-coordinate of 2D grid # return pts, a Numpy array of shape (N,2) - pts = mesh_grid_utils.get_mesh_grid_as_point_cloud(min_x, max_x, min_y, max_y, downsample_factor=1.0) + pts = mesh_grid_utils.get_mesh_grid_as_point_cloud( + min_x, max_x, min_y, max_y, downsample_factor=1.0 + ) assert pts.shape == (6, 2) # fmt: off @@ -71,7 +75,9 @@ def test_get_mesh_grid_as_point_cloud_single_pt() -> None: max_y = 2 # integer, maximum y-coordinate of 2D grid # return pts, a Numpy array of shape (N,2) - pts = mesh_grid_utils.get_mesh_grid_as_point_cloud(min_x, max_x, min_y, max_y, downsample_factor=1.0) + pts = mesh_grid_utils.get_mesh_grid_as_point_cloud( + min_x, max_x, min_y, max_y, downsample_factor=1.0 + ) assert pts.shape == (1, 2) gt_pts: NDArrayFloat = np.array([[-3.0, 2.0]]) @@ -87,7 +93,9 @@ def test_get_mesh_grid_as_point_cloud_downsample() -> None: max_y = 5 # integer, maximum y-coordinate of 2D grid # return pts, a Numpy array of shape (N,2) - pts = mesh_grid_utils.get_mesh_grid_as_point_cloud(min_x, max_x, min_y, max_y, downsample_factor=3.0) + pts = mesh_grid_utils.get_mesh_grid_as_point_cloud( + min_x, max_x, min_y, max_y, downsample_factor=3.0 + ) assert pts.shape == (4, 2) diff --git a/tests/utils/test_metric_time.py b/tests/unit/utils/test_metric_time.py similarity index 100% rename from tests/utils/test_metric_time.py rename to tests/unit/utils/test_metric_time.py diff --git a/tests/utils/test_polyline_utils.py b/tests/unit/utils/test_polyline_utils.py similarity index 86% rename from tests/utils/test_polyline_utils.py rename to tests/unit/utils/test_polyline_utils.py index 1558861a..9f82d55e 100644 --- a/tests/utils/test_polyline_utils.py +++ b/tests/unit/utils/test_polyline_utils.py @@ -22,7 +22,9 @@ def test_convert_lane_boundaries_to_polygon_3d() -> None: [14, -1, 8] ]) # fmt: on - polygon = polyline_utils.convert_lane_boundaries_to_polygon(right_ln_bnd, left_ln_bnd) + polygon = polyline_utils.convert_lane_boundaries_to_polygon( + right_ln_bnd, left_ln_bnd + ) # fmt: off gt_polygon: NDArrayFloat = np.array( @@ -62,19 +64,6 @@ def test_straight_centerline_to_polygon() -> None: # fmt: on polygon = polyline_utils.centerline_to_polygon(centerline, width_scaling_factor=2) - # polygon wraps around with right boundary, then reversed - # left boundary, then back to start vertex - gt_polygon: NDArrayFloat = np.array( - [ - [-3.8, 2.0], - [-3.8, 0.0], - [-3.8, -2.0], - [3.8, -2.0], - [3.8, 0.0], - [3.8, 2.0], - [-3.8, 2.0], - ] - ) # assert np.array_equal(polygon, gt_polygon) assert polygon.shape == (7, 2) diff --git a/tests/utils/test_raster.py b/tests/unit/utils/test_raster.py similarity index 88% rename from tests/utils/test_raster.py rename to tests/unit/utils/test_raster.py index f379d15b..a3d64327 100644 --- a/tests/utils/test_raster.py +++ b/tests/unit/utils/test_raster.py @@ -30,7 +30,9 @@ def test_get_mask_from_polygon() -> None: ] ) # fmt: on - mask = raster_utils.get_mask_from_polygons(polygons=[triangle, rectangle], img_h=7, img_w=7) + mask = raster_utils.get_mask_from_polygons( + polygons=[triangle, rectangle], img_h=7, img_w=7 + ) # fmt: off expected_mask: NDArrayByte = np.array( @@ -73,7 +75,9 @@ def test_get_mask_from_polygon_repeated_coords() -> None: ] ) # fmt: on - mask = raster_utils.get_mask_from_polygons(polygons=[triangle, rectangle], img_h=7, img_w=7) + mask = raster_utils.get_mask_from_polygons( + polygons=[triangle, rectangle], img_h=7, img_w=7 + ) # fmt: off expected_mask: NDArrayByte = np.array( @@ -124,7 +128,9 @@ def test_get_mask_from_polygon_coords_out_of_bounds() -> None: def test_benchmark_blend_images_cv2(benchmark: Callable[..., Any]) -> None: """Benchmark opencv implementation of alpha blending.""" - def blend_images(img0: NDArrayByte, img1: NDArrayByte, alpha: float = 0.7) -> NDArrayByte: + def blend_images( + img0: NDArrayByte, img1: NDArrayByte, alpha: float = 0.7 + ) -> NDArrayByte: """Alpha-blend two images together using OpenCV `addWeighted`. Args: @@ -147,7 +153,9 @@ def blend_images(img0: NDArrayByte, img1: NDArrayByte, alpha: float = 0.7) -> ND def test_benchmark_blend_images_npy(benchmark: Callable[..., Any]) -> None: """Benchmark numpy implementation of alpha blending.""" - def blend_images(img0: NDArrayByte, img1: NDArrayByte, alpha: float = 0.7) -> NDArrayByte: + def blend_images( + img0: NDArrayByte, img1: NDArrayByte, alpha: float = 0.7 + ) -> NDArrayByte: """Alpha-blend two images together using `numpy`. Args: @@ -158,7 +166,9 @@ def blend_images(img0: NDArrayByte, img1: NDArrayByte, alpha: float = 0.7) -> ND Returns: uint8 array of shape (H,W,3) """ - blended: NDArrayFloat = np.multiply(img0.astype(np.float32), alpha, dtype=float) + np.multiply( + blended: NDArrayFloat = np.multiply( + img0.astype(np.float32), alpha, dtype=float + ) + np.multiply( img1.astype(np.float32), (1 - alpha), dtype=float, @@ -187,6 +197,8 @@ def test_blend_images() -> None: img_b: NDArrayByte = np.zeros((H, W, 3), dtype=np.uint8) img_b[:, :, :3] = np.array([2, 4, 8]) # column 0 has uniform intensity - blended_img_expected: NDArrayByte = np.round(img_a * alpha + img_b * beta + gamma).astype(np.uint8) # type: ignore + blended_img_expected: NDArrayByte = np.round( + img_a * alpha + img_b * beta + gamma + ).astype(np.uint8) blended_img = raster_utils.blend_images(img_a, img_b, alpha=alpha) assert np.array_equal(blended_img, blended_img_expected) diff --git a/tests/utils/test_se3.py b/tests/unit/utils/test_se3.py similarity index 90% rename from tests/utils/test_se3.py rename to tests/unit/utils/test_se3.py index f442209e..94737572 100644 --- a/tests/utils/test_se3.py +++ b/tests/unit/utils/test_se3.py @@ -53,7 +53,9 @@ def test_SE3_transform_point_cloud_identity() -> None: def test_SE3_transform_point_cloud_by_quaternion() -> None: """Test rotating points by a given quaternion, and then adding translation vector to each point.""" - pts: NDArrayFloat = np.array([[1.0, 1.0, 1.1], [1.0, 1.0, 2.1], [1.0, 1.0, 3.1], [1.0, 1.0, 4.1]]) + pts: NDArrayFloat = np.array( + [[1.0, 1.0, 1.1], [1.0, 1.0, 2.1], [1.0, 1.0, 3.1], [1.0, 1.0, 4.1]] + ) # x, y, z of cuboid center t: NDArrayFloat = np.array([-34.7128603513203, 5.29461762417753, 0.10328996181488]) @@ -103,7 +105,9 @@ def test_SE3_inverse_transform_point_cloud_identity() -> None: Since the transformation was the identity, the points should not be affected. """ - transformed_pts: NDArrayFloat = np.array([[1.0, 1.0, 1.1], [1.0, 1.0, 2.1], [1.0, 1.0, 3.1], [1.0, 1.0, 4.1]]) + transformed_pts: NDArrayFloat = np.array( + [[1.0, 1.0, 1.1], [1.0, 1.0, 2.1], [1.0, 1.0, 3.1], [1.0, 1.0, 4.1]] + ) dst_se3_src = SE3(rotation=np.eye(3), translation=np.zeros(3)) pts = dst_se3_src.inverse().transform_point_cloud(transformed_pts.copy()) assert np.allclose(pts, transformed_pts) @@ -128,7 +132,9 @@ def test_SE3_inverse_transform_point_cloud() -> None: dst_se3_src = SE3(rotation=R, translation=t) pts = dst_se3_src.inverse().transform_point_cloud(transformed_pts) - gt_pts: NDArrayFloat = np.array([[1.0, 1.0, 1.1], [1.0, 1.0, 2.1], [1.0, 1.0, 3.1], [1.0, 1.0, 4.1]]) + gt_pts: NDArrayFloat = np.array( + [[1.0, 1.0, 1.1], [1.0, 1.0, 2.1], [1.0, 1.0, 3.1], [1.0, 1.0, 4.1]] + ) assert np.allclose(pts, gt_pts) @@ -165,7 +171,9 @@ def test_SE3_chaining_transforms() -> None: assert np.allclose(fr2_se3_fr0.translation, np.zeros(3)) -def test_benchmark_SE3_transform_point_cloud_optimized(benchmark: Callable[..., Any]) -> None: +def test_benchmark_SE3_transform_point_cloud_optimized( + benchmark: Callable[..., Any] +) -> None: """Ensure that our transform_point_cloud() implementation is faster than a naive implementation.""" num_pts = 100000 points_src = np.random.randn(num_pts, 3) @@ -182,7 +190,9 @@ def test_benchmark_SE3_transform_point_cloud_optimized(benchmark: Callable[..., benchmark(dst_SE3_src.transform_point_cloud, points_src) -def test_benchmark_SE3_transform_point_cloud_unoptimized(benchmark: Callable[..., Any]) -> None: +def test_benchmark_SE3_transform_point_cloud_unoptimized( + benchmark: Callable[..., Any] +) -> None: """Benchmark unoptimized SE(3) transformation.""" def benchmark_SE3_transform_point_cloud_unoptimized( @@ -192,7 +202,9 @@ def benchmark_SE3_transform_point_cloud_unoptimized( # convert to homogeneous num_pts = len(point_cloud) homogeneous_pts: NDArrayFloat = np.hstack([point_cloud, np.ones((num_pts, 1))]) - transformed_point_cloud: NDArrayFloat = homogeneous_pts.dot(transform_matrix.T)[:, :3] + transformed_point_cloud: NDArrayFloat = homogeneous_pts.dot(transform_matrix.T)[ + :, :3 + ] return transformed_point_cloud num_pts = 100000 @@ -207,4 +219,8 @@ def benchmark_SE3_transform_point_cloud_unoptimized( dst_SE3_src = SE3(rotation=R.copy(), translation=t.copy()) - benchmark(benchmark_SE3_transform_point_cloud_unoptimized, points_src, dst_SE3_src.transform_matrix) + benchmark( + benchmark_SE3_transform_point_cloud_unoptimized, + points_src, + dst_SE3_src.transform_matrix, + ) diff --git a/tests/utils/test_sim2.py b/tests/unit/utils/test_sim2.py similarity index 96% rename from tests/utils/test_sim2.py rename to tests/unit/utils/test_sim2.py index 47730e23..53e97275 100644 --- a/tests/utils/test_sim2.py +++ b/tests/unit/utils/test_sim2.py @@ -173,7 +173,7 @@ def test_from_matrix() -> None: def test_matrix_homogenous_transform() -> None: - """Ensure 3x3 matrix transforms homogenous points as expected.""" + """Ensure 3x3 matrix transforms homogeneous points as expected.""" expected_img_pts: NDArrayFloat = np.array([[6, 4], [4, 6], [0, 0], [1, 7]]) world_pts: NDArrayFloat = np.array([[2, -1], [1, 0], [-1, -3], [-0.5, 0.5]]) @@ -184,7 +184,7 @@ def test_matrix_homogenous_transform() -> None: world_pts_h: NDArrayFloat = np.hstack([world_pts, np.ones((4, 1))]) # multiply each (3,1) homogeneous point vector w/ transform matrix - img_pts_h = (imgSw.matrix @ world_pts_h.T).T # type: ignore + img_pts_h = (imgSw.matrix @ world_pts_h.T).T # divide (x,y,s) by s img_pts = img_pts_h[:, :2] / img_pts_h[:, 2].reshape(-1, 1) assert np.allclose(expected_img_pts, img_pts) @@ -206,7 +206,9 @@ def test_transform_from_backwards() -> None: """Test Similarity(2) backward transform.""" img_pts: NDArrayFloat = np.array([[6, 4], [4, 6], [0, 0], [1, 7]]) - expected_world_pts: NDArrayFloat = np.array([[2, -1], [1, 0], [-1, -3], [-0.5, 0.5]]) + expected_world_pts: NDArrayFloat = np.array( + [[2, -1], [1, 0], [-1, -3], [-0.5, 0.5]] + ) scale = 0.5 wSimg = Sim2(R=np.eye(2), t=np.array([-2.0, -6.0]), s=scale) @@ -228,7 +230,7 @@ def test_cannot_set_zero_scale() -> None: t: NDArrayFloat = np.arange(2, dtype=float) s = 0.0 - with pytest.raises(ZeroDivisionError) as e_info: + with pytest.raises(ZeroDivisionError): Sim2(R, t, s) @@ -288,7 +290,7 @@ def test_from_json_invalid_scale() -> None: """Ensure that classmethod raises an error with invalid JSON input.""" json_fpath = _TEST_DATA_ROOT / "a_Sim2_b___invalid.json" - with pytest.raises(ZeroDivisionError) as e_info: + with pytest.raises(ZeroDivisionError): Sim2.from_json(json_fpath) diff --git a/tutorials/3d_object_detection.py b/tutorials/3d_object_detection.py new file mode 100644 index 00000000..ff6d9cc5 --- /dev/null +++ b/tutorials/3d_object_detection.py @@ -0,0 +1,78 @@ +"""Example of Rust-backed, PyTorch data-loader.""" + +import logging +from pathlib import Path +from typing import Final + +from kornia.geometry.linalg import transform_points +from tqdm import tqdm + +from av2.torch.data_loaders.detection import DetectionDataLoader + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +HOME_DIR: Final = Path.home() + + +def main( + root_dir: Path = HOME_DIR / "data" / "datasets", + dataset_name: str = "av2", + split_name: str = "val", + num_accumulated_sweeps: int = 1, + max_iterations: int = 10000, +) -> None: + """Iterate over the detection data-loader. + + Dataset should live at ~/data/datasets/{dataset_name}/{split_name}. + + Args: + root_dir: Root directory to the datasets. + dataset_name: Name of the dataset (e.g., "av2"). + split_name: Name of the split (e.g., "val"). + num_accumulated_sweeps: Number of sweeps to accumulate. + max_iterations: Maximum number of iterations for the data-loader example. + """ + logger.info("Starting detection data-loader example ...") + data_loader = DetectionDataLoader( + root_dir, + dataset_name, + split_name, + num_accumulated_sweeps=num_accumulated_sweeps, + ) + for i, sweep in enumerate(tqdm(data_loader)): + # 4x4 matrix representing the SE(3) transformation to city from ego-vehicle coordinates. + city_SE3_ego_mat4 = sweep.city_SE3_ego.matrix() + + # Lidar (x,y,z) in meters and intensity (i). + lidar_tensor = sweep.lidar.as_tensor() + + # Synchronized ring cameras. + synchronized_images = data_loader.get_synchronized_images(i) + + # Transform the points to city coordinates. + lidar_xyz_city = transform_points(city_SE3_ego_mat4, lidar_tensor[:, :3]) + + # Cuboids might not be available (e.g., using the "test" split). + if sweep.cuboids is not None: + # Annotations in (x,y,z,l,w,h,yaw) format. + # 1-DOF rotation. + xyzlwh_t = sweep.cuboids.as_tensor() + + # Access cuboid category. + category = sweep.cuboids.category + + # Access track uuid. + track_uuid = sweep.cuboids.track_uuid + + # print(lidar_xyz_city, xyzlwh_t, category, track_uuid) + + if i >= max_iterations: + logger.info(f"Reached max iterations of {max_iterations}!") + break + + logger.info("Example complete!") + + +if __name__ == "__main__": + main() diff --git a/tutorials/generate_egoview_overlaid_lidar.py b/tutorials/generate_egoview_overlaid_lidar.py index 8e91a8aa..dd1101e4 100644 --- a/tutorials/generate_egoview_overlaid_lidar.py +++ b/tutorials/generate_egoview_overlaid_lidar.py @@ -31,7 +31,11 @@ def generate_egoview_overlaid_lidar( - data_root: Path, output_dir: Path, log_id: str, render_ground_pts_only: bool, dump_single_frames: bool + data_root: Path, + output_dir: Path, + log_id: str, + render_ground_pts_only: bool, + dump_single_frames: bool, ) -> None: """Render LiDAR points from a particular camera's viewpoint (color by ground surface, and apply ROI filtering). @@ -52,9 +56,11 @@ def generate_egoview_overlaid_lidar( avm = ArgoverseStaticMap.from_map_dir(log_map_dirpath, build_raster=True) # repeat red to green colormap every 50 m. - colors_arr_rgb = color_utils.create_colormap(color_list=[RED_HEX, GREEN_HEX], n_colors=NUM_RANGE_BINS) - colors_arr_rgb = (colors_arr_rgb * 255).astype(np.uint8) # type: ignore - colors_arr_bgr: NDArrayByte = np.fliplr(colors_arr_rgb) # type: ignore + colors_arr_rgb = color_utils.create_colormap( + color_list=[RED_HEX, GREEN_HEX], n_colors=NUM_RANGE_BINS + ) + colors_arr_rgb = (colors_arr_rgb * 255).astype(np.uint8) + colors_arr_bgr: NDArrayByte = np.fliplr(colors_arr_rgb) for _, cam_name in enumerate(list(RingCameras)): cam_im_fpaths = loader.get_ordered_log_cam_fpaths(log_id, cam_name) @@ -63,7 +69,9 @@ def generate_egoview_overlaid_lidar( video_list = [] for i, im_fpath in enumerate(cam_im_fpaths): if i % 50 == 0: - logging.info(f"\tOn file {i}/{num_cam_imgs} of camera {cam_name} of {log_id}") + logging.info( + f"\tOn file {i}/{num_cam_imgs} of camera {cam_name} of {log_id}" + ) cam_timestamp_ns = int(im_fpath.stem) city_SE3_ego = loader.get_city_SE3_ego(log_id, cam_timestamp_ns) @@ -74,7 +82,10 @@ def generate_egoview_overlaid_lidar( # load feather file path, e.g. '315978406032859416.feather" lidar_fpath = loader.get_closest_lidar_fpath(log_id, cam_timestamp_ns) if lidar_fpath is None: - logger.info("No LiDAR sweep found within the synchronization interval for %s, so skipping...", cam_name) + logger.info( + "No LiDAR sweep found within the synchronization interval for %s, so skipping...", + cam_name, + ) continue img_bgr = io_utils.read_img(im_fpath, channel_order="BGR") @@ -86,11 +97,19 @@ def generate_egoview_overlaid_lidar( lidar_points_city = city_SE3_ego.transform_point_cloud(lidar_points_ego) lidar_points_city = avm.remove_non_drivable_area_points(lidar_points_city) is_ground_logicals = avm.get_ground_points_boolean(lidar_points_city) - lidar_points_city = lidar_points_city[is_ground_logicals if render_ground_pts_only else ~is_ground_logicals] - lidar_points_ego = city_SE3_ego.inverse().transform_point_cloud(lidar_points_city) + lidar_points_city = lidar_points_city[ + is_ground_logicals if render_ground_pts_only else ~is_ground_logicals + ] + lidar_points_ego = city_SE3_ego.inverse().transform_point_cloud( + lidar_points_city + ) # motion compensate always - uv, points_cam, is_valid_points = loader.project_ego_to_img_motion_compensated( + ( + uv, + points_cam, + is_valid_points, + ) = loader.project_ego_to_img_motion_compensated( points_lidar_time=lidar_points_ego, cam_name=cam_name, cam_timestamp_ns=cam_timestamp_ns, @@ -104,35 +123,47 @@ def generate_egoview_overlaid_lidar( if is_valid_points.sum() == 0: continue - uv_int: NDArrayInt = np.round(uv[is_valid_points]).astype(np.int32) # type: ignore + uv_int: NDArrayInt = np.round(uv[is_valid_points]).astype(np.int32) points_cam = points_cam[is_valid_points] - pt_ranges: NDArrayFloat = np.linalg.norm(points_cam[:, :3], axis=1) # type: ignore - color_bins: NDArrayInt = np.round(pt_ranges).astype(np.int32) # type: ignore + pt_ranges: NDArrayFloat = np.linalg.norm(points_cam[:, :3], axis=1) + color_bins: NDArrayInt = np.round(pt_ranges).astype(np.int32) # account for moving past 100 meters, loop around again color_bins = color_bins % NUM_RANGE_BINS uv_colors_bgr = colors_arr_bgr[color_bins] img_empty = np.full_like(img_bgr, fill_value=255) - img_empty = raster_rendering_utils.draw_points_xy_in_img(img_empty, uv_int, uv_colors_bgr, diameter=10) + img_empty = raster_rendering_utils.draw_points_xy_in_img( + img_empty, uv_int, uv_colors_bgr, diameter=10 + ) blended_bgr = raster_utils.blend_images(img_bgr, img_empty) frame_rgb = blended_bgr[:, :, ::-1] if dump_single_frames: save_dir = output_dir / log_id / cam_name os.makedirs(save_dir, exist_ok=True) - cv2.imwrite(str(save_dir / f"{cam_name}_{lidar_timestamp_ns}.jpg"), blended_bgr) + cv2.imwrite( + str(save_dir / f"{cam_name}_{lidar_timestamp_ns}.jpg"), blended_bgr + ) video_list.append(frame_rgb) if len(video_list) == 0: - raise RuntimeError("No video frames were found; log data was not found on disk.") + raise RuntimeError( + "No video frames were found; log data was not found on disk." + ) video: NDArrayByte = np.stack(video_list).astype(np.uint8) video_output_dir = output_dir / "videos" - video_utils.write_video(video=video, dst=video_output_dir / f"{log_id}_{cam_name}.mp4", fps=RING_CAMERA_FPS) + video_utils.write_video( + video=video, + dst=video_output_dir / f"{log_id}_{cam_name}.mp4", + fps=RING_CAMERA_FPS, + ) -@click.command(help="Generate LiDAR + map visualizations from the Argoverse 2 Sensor Dataset.") +@click.command( + help="Generate LiDAR + map visualizations from the Argoverse 2 Sensor Dataset." +) @click.option( "-d", "--data-root", @@ -170,7 +201,11 @@ def generate_egoview_overlaid_lidar( type=bool, ) def run_generate_egoview_overlaid_lidar( - data_root: str, output_dir: str, log_id: str, render_ground_pts_only: bool, dump_single_frames: bool + data_root: str, + output_dir: str, + log_id: str, + render_ground_pts_only: bool, + dump_single_frames: bool, ) -> None: """Click entry point for visualizing LiDAR returns rendered on top of sensor imagery.""" logging.basicConfig(stream=sys.stdout, level=logging.INFO) diff --git a/tutorials/generate_egoview_overlaid_vector_map.py b/tutorials/generate_egoview_overlaid_vector_map.py index aa79f364..ebbf2ce2 100644 --- a/tutorials/generate_egoview_overlaid_vector_map.py +++ b/tutorials/generate_egoview_overlaid_vector_map.py @@ -67,7 +67,9 @@ def generate_egoview_overlaid_map( video_list = [] for i, img_fpath in enumerate(cam_im_fpaths): if i % 50 == 0: - logging.info(f"\tOn file {i}/{num_cam_imgs} of camera {cam_name} of {log_id}") + logging.info( + f"\tOn file {i}/{num_cam_imgs} of camera {cam_name} of {log_id}" + ) cam_timestamp_ns = int(img_fpath.stem) city_SE3_ego = loader.get_city_SE3_ego(log_id, cam_timestamp_ns) @@ -96,7 +98,10 @@ def generate_egoview_overlaid_map( depth_map = None egoview_renderer = EgoViewMapRenderer( - depth_map=depth_map, city_SE3_ego=city_SE3_ego, pinhole_cam=pinhole_cam, avm=avm + depth_map=depth_map, + city_SE3_ego=city_SE3_ego, + pinhole_cam=pinhole_cam, + avm=avm, ) frame_rgb = render_egoview( output_dir=output_dir, @@ -148,7 +153,9 @@ def render_egoview( # we only create log-specific directories, if dumping individual frames. save_dir.mkdir(exist_ok=True, parents=True) - img_fname = f"{egoview_renderer.pinhole_cam.cam_name}_{cam_timestamp_ns}_vectormap.jpg" + img_fname = ( + f"{egoview_renderer.pinhole_cam.cam_name}_{cam_timestamp_ns}_vectormap.jpg" + ) save_fpath = save_dir / img_fname if save_fpath.exists(): @@ -183,7 +190,10 @@ def render_egoview( def render_egoview_with_occlusion_checks( - img_canvas: NDArrayByte, egoview_renderer: EgoViewMapRenderer, max_range_m: float, line_width_px: int = 10 + img_canvas: NDArrayByte, + egoview_renderer: EgoViewMapRenderer, + max_range_m: float, + line_width_px: int = 10, ) -> NDArrayByte: """Render pedestrian crossings and lane segments in the ego-view. @@ -200,11 +210,14 @@ def render_egoview_with_occlusion_checks( array of shape (H,W,3) and type uint8 representing a RGB image. """ for ls in egoview_renderer.avm.get_scenario_lane_segments(): - img_canvas = egoview_renderer.render_lane_boundary_egoview(img_canvas, ls, "right", line_width_px) - img_canvas = egoview_renderer.render_lane_boundary_egoview(img_canvas, ls, "left", line_width_px) + img_canvas = egoview_renderer.render_lane_boundary_egoview( + img_canvas, ls, "right", line_width_px + ) + img_canvas = egoview_renderer.render_lane_boundary_egoview( + img_canvas, ls, "left", line_width_px + ) for pc in egoview_renderer.avm.get_scenario_ped_crossings(): - EPS = 1e-5 crosswalk_color = BLUE_BGR # render ped crossings (pc's) @@ -214,8 +227,12 @@ def render_egoview_with_occlusion_checks( N_INTERP_PTS = 100 # For pixel-perfect rendering, querying crosswalk boundary ground height at waypoints throughout # the street is much more accurate than 3d linear interpolation using only the 4 annotated corners. - polygon_city_frame = interp_utils.interp_arc(t=N_INTERP_PTS, points=xwalk_polygon[:, :2]) - polygon_city_frame = egoview_renderer.avm.append_height_to_2d_city_pt_cloud(points_xy=polygon_city_frame) + polygon_city_frame = interp_utils.interp_arc( + t=N_INTERP_PTS, points=xwalk_polygon[:, :2] + ) + polygon_city_frame = egoview_renderer.avm.append_height_to_2d_city_pt_cloud( + points_xy=polygon_city_frame + ) egoview_renderer.render_polyline_egoview( polygon_city_frame, img_canvas, @@ -251,9 +268,10 @@ def parse_camera_enum_types(cam_names: Tuple[str, ...]) -> List[RingCameras]: return cam_enums -@click.command(help="Generate map visualizations on ego-view imagery from the Argoverse 2 Sensor or TbV Datasets.") +@click.command( + help="Generate map visualizations on ego-view imagery from the Argoverse 2 Sensor or TbV Datasets." +) @click.option( - "-d", "--data-root", required=True, help="Path to local directory where the Argoverse 2 Sensor Dataset or TbV logs are stored.", diff --git a/tutorials/generate_forecasting_scenario_visualizations.py b/tutorials/generate_forecasting_scenario_visualizations.py index f94de042..c2273670 100644 --- a/tutorials/generate_forecasting_scenario_visualizations.py +++ b/tutorials/generate_forecasting_scenario_visualizations.py @@ -3,15 +3,17 @@ from enum import Enum, unique from pathlib import Path +from random import choices from typing import Final import click -import numpy as np from joblib import Parallel, delayed from rich.progress import track from av2.datasets.motion_forecasting import scenario_serialization -from av2.datasets.motion_forecasting.viz.scenario_visualization import visualize_scenario +from av2.datasets.motion_forecasting.viz.scenario_visualization import ( + visualize_scenario, +) from av2.map.map_api import ArgoverseStaticMap _DEFAULT_N_JOBS: Final[int] = -2 # Use all but one CPUs @@ -47,7 +49,7 @@ def generate_scenario_visualizations( scenario_file_list = ( all_scenario_files[:num_scenarios] if selection_criteria == SelectionCriteria.FIRST - else np.random.choice(all_scenario_files, size=num_scenarios).tolist() # type: ignore + else choices(all_scenario_files, k=num_scenarios) ) # Ignoring type here because type of "choice" is partially unknown. # Build inner function to generate visualization for a single scenario. @@ -60,7 +62,9 @@ def generate_scenario_visualization(scenario_path: Path) -> None: scenario_path: Path to the parquet file corresponding to the Argoverse scenario to visualize. """ scenario_id = scenario_path.stem.split("_")[-1] - static_map_path = scenario_path.parents[0] / f"log_map_archive_{scenario_id}.json" + static_map_path = ( + scenario_path.parents[0] / f"log_map_archive_{scenario_id}.json" + ) viz_save_path = viz_output_dir / f"{scenario_id}.mp4" scenario = scenario_serialization.load_argoverse_scenario_parquet(scenario_path) @@ -73,7 +77,8 @@ def generate_scenario_visualization(scenario_path: Path) -> None: generate_scenario_visualization(scenario_path) else: Parallel(n_jobs=_DEFAULT_N_JOBS)( - delayed(generate_scenario_visualization)(scenario_path) for scenario_path in track(scenario_file_list) + delayed(generate_scenario_visualization)(scenario_path) + for scenario_path in track(scenario_file_list) ) @@ -104,9 +109,18 @@ def generate_scenario_visualization(scenario_path: Path) -> None: help="Controls how scenarios are selected for visualization - either the first available or at random.", type=click.Choice(["first", "random"], case_sensitive=False), ) -@click.option("--debug", is_flag=True, default=False, help="Runs preprocessing in single-threaded mode when enabled.") +@click.option( + "--debug", + is_flag=True, + default=False, + help="Runs preprocessing in single-threaded mode when enabled.", +) def run_generate_scenario_visualizations( - argoverse_scenario_dir: str, viz_output_dir: str, num_scenarios: int, selection_criteria: str, debug: bool + argoverse_scenario_dir: str, + viz_output_dir: str, + num_scenarios: int, + selection_criteria: str, + debug: bool, ) -> None: """Click entry point for generation of Argoverse scenario visualizations.""" generate_scenario_visualizations( diff --git a/tutorials/generate_per_camera_videos.py b/tutorials/generate_per_camera_videos.py index 45e45079..9a1bda2f 100644 --- a/tutorials/generate_per_camera_videos.py +++ b/tutorials/generate_per_camera_videos.py @@ -25,7 +25,9 @@ logger = logging.getLogger(__name__) -def generate_per_camera_videos(data_root: Path, output_dir: Path, num_workers: int) -> None: +def generate_per_camera_videos( + data_root: Path, output_dir: Path, num_workers: int +) -> None: """Launch jobs to render ring camera .mp4 videos for all sensor logs available on disk. Args: @@ -38,14 +40,17 @@ def generate_per_camera_videos(data_root: Path, output_dir: Path, num_workers: i if num_workers > 1: Parallel(n_jobs=num_workers)( - delayed(render_log_ring_camera_videos)(output_dir, loader, log_id) for log_id in log_ids + delayed(render_log_ring_camera_videos)(output_dir, loader, log_id) + for log_id in log_ids ) else: for log_id in log_ids: render_log_ring_camera_videos(output_dir, loader, log_id) -def render_log_ring_camera_videos(output_dir: Path, loader: AV2SensorDataLoader, log_id: str) -> None: +def render_log_ring_camera_videos( + output_dir: Path, loader: AV2SensorDataLoader, log_id: str +) -> None: """Render .mp4 videos for all ring cameras of a single log. Args: @@ -60,7 +65,9 @@ def render_log_ring_camera_videos(output_dir: Path, loader: AV2SensorDataLoader, for camera_name in list(RingCameras): video_save_fpath = Path(output_dir) / f"{log_id}_{camera_name}.mp4" if video_save_fpath.exists(): - logger.info("Video already exists for %s, %s, so skipping...", log_id, camera_name) + logger.info( + "Video already exists for %s, %s, so skipping...", log_id, camera_name + ) continue cam_im_fpaths = loader.get_ordered_log_cam_fpaths(log_id, camera_name) @@ -69,7 +76,9 @@ def render_log_ring_camera_videos(output_dir: Path, loader: AV2SensorDataLoader, video_list: List[NDArrayByte] = [] for i, im_fpath in enumerate(cam_im_fpaths): if i % PRINT_EVERY == 0: - logger.info(f"\tOn file {i}/{num_cam_imgs} of camera {camera_name} of {log_id}") + logger.info( + f"\tOn file {i}/{num_cam_imgs} of camera {camera_name} of {log_id}" + ) img_rgb = io_utils.read_img(im_fpath, channel_order="RGB") video_list.append(img_rgb) @@ -82,7 +91,9 @@ def render_log_ring_camera_videos(output_dir: Path, loader: AV2SensorDataLoader, ) -@click.command(help="Generate map visualizations on ego-view imagery from the Argoverse 2 Sensor or TbV Datasets.") +@click.command( + help="Generate map visualizations on ego-view imagery from the Argoverse 2 Sensor or TbV Datasets." +) @click.option( "-d", "--data-root", @@ -103,10 +114,14 @@ def render_log_ring_camera_videos(output_dir: Path, loader: AV2SensorDataLoader, help="Number of worker processes to use for rendering.", type=int, ) -def run_generate_per_camera_videos(data_root: str, output_dir: str, num_workers: int) -> None: +def run_generate_per_camera_videos( + data_root: str, output_dir: str, num_workers: int +) -> None: """Click entry point for ring camera .mp4 video generation.""" logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) - generate_per_camera_videos(data_root=Path(data_root), output_dir=Path(output_dir), num_workers=num_workers) + generate_per_camera_videos( + data_root=Path(data_root), output_dir=Path(output_dir), num_workers=num_workers + ) if __name__ == "__main__": diff --git a/tutorials/generate_sensor_dataset_visualizations.py b/tutorials/generate_sensor_dataset_visualizations.py index caa9a515..c8a188fa 100644 --- a/tutorials/generate_sensor_dataset_visualizations.py +++ b/tutorials/generate_sensor_dataset_visualizations.py @@ -24,7 +24,9 @@ # Model an xy grid in the Bird's-eye view. BEV_GRID: Final[BEVGrid] = BEVGrid( - min_range_m=MIN_RANGE_M, max_range_m=MAX_RANGE_M, resolution_m_per_cell=RESOLUTION_M_PER_CELL + min_range_m=MIN_RANGE_M, + max_range_m=MAX_RANGE_M, + resolution_m_per_cell=RESOLUTION_M_PER_CELL, ) @@ -51,6 +53,8 @@ def generate_sensor_dataset_visualizations( for _, datum in enumerate(track(dataset, "Creating sensor tutorial videos ...")): sweep = datum.sweep annotations = datum.annotations + if annotations is None: + continue timestamp_city_SE3_ego_dict = datum.timestamp_city_SE3_ego_dict synchronized_imagery = datum.synchronized_imagery @@ -62,22 +66,37 @@ def generate_sensor_dataset_visualizations( and sweep.timestamp_ns in timestamp_city_SE3_ego_dict ): city_SE3_ego_cam_t = timestamp_city_SE3_ego_dict[cam.timestamp_ns] - city_SE3_ego_lidar_t = timestamp_city_SE3_ego_dict[sweep.timestamp_ns] - - uv, points_cam, is_valid_points = cam.camera_model.project_ego_to_img_motion_compensated( + city_SE3_ego_lidar_t = timestamp_city_SE3_ego_dict[ + sweep.timestamp_ns + ] + + ( + uv, + points_cam, + is_valid_points, + ) = cam.camera_model.project_ego_to_img_motion_compensated( sweep.xyz, city_SE3_ego_cam_t=city_SE3_ego_cam_t, city_SE3_ego_lidar_t=city_SE3_ego_lidar_t, ) - uv_int: NDArrayInt = np.round(uv[is_valid_points]).astype(int) # type: ignore + uv_int: NDArrayInt = np.round(uv[is_valid_points]).astype(int) colors = create_range_map(points_cam[is_valid_points, :3]) img = draw_points_xy_in_img( - cam.img, uv_int, colors=colors, alpha=0.85, diameter=5, sigma=1.0, with_anti_alias=True + cam.img, + uv_int, + colors=colors, + alpha=0.85, + diameter=5, + sigma=1.0, + with_anti_alias=True, ) if annotations is not None: img = annotations.project_to_cam( - img, cam.camera_model, city_SE3_ego_cam_t, city_SE3_ego_lidar_t + img, + cam.camera_model, + city_SE3_ego_cam_t, + city_SE3_ego_lidar_t, ) cam_name_to_img[cam_name] = img if len(cam_name_to_img) < len(cam_names): diff --git a/tutorials/map_teaser_notebook.py b/tutorials/map_teaser_notebook.py index d37fc942..dc041179 100644 --- a/tutorials/map_teaser_notebook.py +++ b/tutorials/map_teaser_notebook.py @@ -27,7 +27,11 @@ # scaled to [0,1] for matplotlib. PURPLE_RGB: Final[Tuple[int, int, int]] = (201, 71, 245) -PURPLE_RGB_MPL: Final[Tuple[float, float, float]] = (PURPLE_RGB[0] / 255, PURPLE_RGB[1] / 255, PURPLE_RGB[2] / 255) +PURPLE_RGB_MPL: Final[Tuple[float, float, float]] = ( + PURPLE_RGB[0] / 255, + PURPLE_RGB[1] / 255, + PURPLE_RGB[2] / 255, +) DARK_GRAY_RGB: Final[Tuple[int, int, int]] = (40, 39, 38) DARK_GRAY_RGB_MPL: Final[Tuple[float, float, float]] = ( @@ -40,7 +44,7 @@ def single_log_teaser(data_root: Path, log_id: str, save_figures: bool) -> None: - """For a single log, render all local lane segments in green, and pedestrian crossings in purple, in a bird's eye view. + """Render all local lane segments in green, and pedestrian crossings in purple, in a bird's eye view. Args: data_root: path to where the AV2 logs live. @@ -54,13 +58,19 @@ def single_log_teaser(data_root: Path, log_id: str, save_figures: bool) -> None: ax = fig.add_subplot() for _, ls in avm.vector_lane_segments.items(): - vector_plotting_utils.draw_polygon_mpl(ax, ls.polygon_boundary, color="g", linewidth=0.5) - vector_plotting_utils.plot_polygon_patch_mpl(ls.polygon_boundary, ax, color="g", alpha=0.2) + vector_plotting_utils.draw_polygon_mpl( + ax, ls.polygon_boundary, color="g", linewidth=0.5 + ) + vector_plotting_utils.plot_polygon_patch_mpl( + ls.polygon_boundary, ax, color="g", alpha=0.2 + ) # plot all pedestrian crossings for _, pc in avm.vector_pedestrian_crossings.items(): vector_plotting_utils.draw_polygon_mpl(ax, pc.polygon, color="m", linewidth=0.5) - vector_plotting_utils.plot_polygon_patch_mpl(pc.polygon, ax, color="m", alpha=0.2) + vector_plotting_utils.plot_polygon_patch_mpl( + pc.polygon, ax, color="m", alpha=0.2 + ) plt.axis("equal") plt.tight_layout() @@ -73,7 +83,9 @@ def single_log_teaser(data_root: Path, log_id: str, save_figures: bool) -> None: ax = fig.add_subplot() for da in list(avm.vector_drivable_areas.values()): vector_plotting_utils.draw_polygon_mpl(ax, da.xyz, color="gray", linewidth=0.5) - vector_plotting_utils.plot_polygon_patch_mpl(da.xyz, ax, color="gray", alpha=0.2) + vector_plotting_utils.plot_polygon_patch_mpl( + da.xyz, ax, color="gray", alpha=0.2 + ) plt.axis("equal") plt.tight_layout() @@ -110,7 +122,7 @@ def visualize_raster_layers(data_root: Path, log_id: str, save_figures: bool) -> height_array = avm.raster_ground_height_layer.array ax = plt.subplot() plt.title("Ground surface height (@ 30 centimeter resolution).") - img = plt.imshow(np.flipud(height_array)) # type: ignore + img = plt.imshow(np.flipud(height_array)) divider = make_axes_locatable(ax) cax = divider.append_axes("right", size="5%", pad=0.05) @@ -122,17 +134,17 @@ def visualize_raster_layers(data_root: Path, log_id: str, save_figures: bool) -> fig = plt.figure(figsize=(10, 10)) plt.subplot(1, 3, 1) - plt.imshow(np.flipud(height_array)) # type: ignore + plt.imshow(np.flipud(height_array)) plt.title("Ground Surface Height") plt.subplot(1, 3, 2) da_array = avm.raster_drivable_area_layer.array - plt.imshow(np.flipud(da_array)) # type: ignore + plt.imshow(np.flipud(da_array)) plt.title("Drivable Area (rasterized \nfrom vector polygons)") plt.subplot(1, 3, 3) roi_array = avm.raster_roi_layer.array - plt.imshow(np.flipud(roi_array)) # type: ignore + plt.imshow(np.flipud(roi_array)) plt.title("Region of Interest (ROI)") fig.tight_layout() @@ -176,7 +188,8 @@ def overlaid_maps_all_logs_teaser(data_root: Path) -> None: pts_ego = city_SE3_egot0.inverse().transform_point_cloud(pts_city) for bound_type, bound_city in zip( - [ls.left_mark_type, ls.right_mark_type], [ls.left_lane_boundary, ls.right_lane_boundary] + [ls.left_mark_type, ls.right_mark_type], + [ls.left_lane_boundary, ls.right_lane_boundary], ): if "YELLOW" in bound_type: mark_color = "y" @@ -194,7 +207,9 @@ def overlaid_maps_all_logs_teaser(data_root: Path) -> None: else: linestyle = "solid" - bound_ego = city_SE3_egot0.inverse().transform_point_cloud(bound_city.xyz) + bound_ego = city_SE3_egot0.inverse().transform_point_cloud( + bound_city.xyz + ) ax.plot( bound_ego[:, 0], bound_ego[:, 1], @@ -205,7 +220,11 @@ def overlaid_maps_all_logs_teaser(data_root: Path) -> None: ) vector_plotting_utils.plot_polygon_patch_mpl( - polygon_pts=pts_ego, ax=ax, color=color, alpha=OVERLAID_MAPS_ALPHA, zorder=i + polygon_pts=pts_ego, + ax=ax, + color=color, + alpha=OVERLAID_MAPS_ALPHA, + zorder=i, ) plt.axis("equal") @@ -216,7 +235,9 @@ def overlaid_maps_all_logs_teaser(data_root: Path) -> None: def plot_lane_segments( - ax: Axes, lane_segments: Sequence[LaneSegment], lane_color: Tuple[float, float, float] = DARK_GRAY_RGB_MPL + ax: Axes, + lane_segments: Sequence[LaneSegment], + lane_color: Tuple[float, float, float] = DARK_GRAY_RGB_MPL, ) -> None: """Plot lane segments onto a Matplotlib canvas, according to their lane marking boundary type/color. @@ -237,7 +258,8 @@ def plot_lane_segments( mark_color: str = "" linestyle: Union[str, Tuple[int, Tuple[int, int]]] = "" for bound_type, bound_city in zip( - [ls.left_mark_type, ls.right_mark_type], [ls.left_lane_boundary, ls.right_lane_boundary] + [ls.left_mark_type, ls.right_mark_type], + [ls.left_lane_boundary, ls.right_lane_boundary], ): if "YELLOW" in bound_type: mark_color = "y" @@ -259,8 +281,22 @@ def plot_lane_segments( left, right = polyline_utils.get_double_polylines( polyline=bound_city.xyz[:, :2], width_scaling_factor=0.1 ) - ax.plot(left[:, 0], left[:, 1], color=mark_color, alpha=ALPHA, linestyle=linestyle, zorder=2) - ax.plot(right[:, 0], right[:, 1], color=mark_color, alpha=ALPHA, linestyle=linestyle, zorder=2) + ax.plot( + left[:, 0], + left[:, 1], + color=mark_color, + alpha=ALPHA, + linestyle=linestyle, + zorder=2, + ) + ax.plot( + right[:, 0], + right[:, 1], + color=mark_color, + alpha=ALPHA, + linestyle=linestyle, + zorder=2, + ) else: ax.plot( bound_city.xyz[:, 0], @@ -272,7 +308,9 @@ def plot_lane_segments( ) -def visualize_ego_pose_and_lane_markings(data_root: Path, log_id: str, save_figures: bool) -> None: +def visualize_ego_pose_and_lane_markings( + data_root: Path, log_id: str, save_figures: bool +) -> None: """Visualize both ego-vehicle poses and the per-log local vector map. Crosswalks are plotted in purple. Lane segments plotted in dark gray. Ego-pose in red. @@ -294,12 +332,12 @@ def visualize_ego_pose_and_lane_markings(data_root: Path, log_id: str, save_figu traj_ns = loader.get_subsampled_ego_trajectory(log_id, sample_rate_hz=1e9) # now, sample @ 1 Hz traj_1hz = loader.get_subsampled_ego_trajectory(log_id, sample_rate_hz=1.0) - med: NDArrayFloat = np.median(traj_ns, axis=0) # type: ignore + med: NDArrayFloat = np.median(traj_ns, axis=0) med_x, med_y = med # Derive plot area from trajectory (with radius defined in infinity norm). # A larger distance traveled during trajectory means we should have a larger viewing window size. - view_radius_m: float = float(np.linalg.norm(traj_ns[-1] - traj_ns[0])) + 20 # type: ignore + view_radius_m: float = float(np.linalg.norm(traj_ns[-1] - traj_ns[0])) + 20 xlims = [med_x - view_radius_m, med_x + view_radius_m] ylims = [med_y - view_radius_m, med_y + view_radius_m] @@ -318,7 +356,15 @@ def visualize_ego_pose_and_lane_markings(data_root: Path, log_id: str, save_figu # Plot nearly continuous line for ego-pose, and show the AV's pose @ 1 Hz w/ red unfilled circles. ax.plot(traj_ns[:, 0], traj_ns[:, 1], color="r", zorder=4, label="Ego-vehicle pose") - ax.scatter(x=traj_1hz[:, 0], y=traj_1hz[:, 1], s=100, marker="o", facecolors="none", edgecolors="r", zorder=4) + ax.scatter( + x=traj_1hz[:, 0], + y=traj_1hz[:, 1], + s=100, + marker="o", + facecolors="none", + edgecolors="r", + zorder=4, + ) plt.axis("equal") plt.xlim(*xlims) @@ -362,9 +408,15 @@ def run_map_tutorial(data_root: str, log_id: str, save_figures: bool) -> None: SAVE_DIR.mkdir(exist_ok=True, parents=True) logger.info("data_root: %s, log_id: %s", data_root_path, log_id) - single_log_teaser(data_root=data_root_path, log_id=log_id, save_figures=save_figures) - visualize_raster_layers(data_root=data_root_path, log_id=log_id, save_figures=save_figures) - visualize_ego_pose_and_lane_markings(data_root=data_root_path, log_id=log_id, save_figures=save_figures) + single_log_teaser( + data_root=data_root_path, log_id=log_id, save_figures=save_figures + ) + visualize_raster_layers( + data_root=data_root_path, log_id=log_id, save_figures=save_figures + ) + visualize_ego_pose_and_lane_markings( + data_root=data_root_path, log_id=log_id, save_figures=save_figures + ) overlaid_maps_all_logs_teaser(data_root=data_root_path) diff --git a/tutorials/map_tutorial.ipynb b/tutorials/map_tutorial.ipynb index f68e27b6..6367577a 100644 --- a/tutorials/map_tutorial.ipynb +++ b/tutorials/map_tutorial.ipynb @@ -109,7 +109,6 @@ " ax = fig.add_subplot()\n", "\n", " for _, ls in avm.vector_lane_segments.items():\n", - "\n", " # right_ln_bnd\n", " # left_ln_bnd\n", " vector_plotting_utils.draw_polygon_mpl(ax, ls.polygon_boundary, color=\"g\", linewidth=0.5)\n", diff --git a/tutorials/untar_tbv.py b/tutorials/untar_tbv.py index df564359..38666fb2 100644 --- a/tutorials/untar_tbv.py +++ b/tutorials/untar_tbv.py @@ -12,8 +12,10 @@ NUM_TBV_SHARDS: Final[int] = 21 -def run_command(cmd: str, return_output: bool = False) -> Tuple[Optional[bytes], Optional[bytes]]: - """Excute a system call, and block until the system call completes. +def run_command( + cmd: str, return_output: bool = False +) -> Tuple[Optional[bytes], Optional[bytes]]: + """Execute a system call, and block until the system call completes. Args: cmd: string, representing shell command @@ -23,14 +25,18 @@ def run_command(cmd: str, return_output: bool = False) -> Tuple[Optional[bytes], Tuple of (stdout, stderr) output if return_output is True, else None """ print(cmd) - (stdout_data, stderr_data) = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate() + (stdout_data, stderr_data) = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE + ).communicate() if return_output: return stdout_data, stderr_data return None, None -def untar_tbv_dataset(num_workers: int, shard_dirpath: Path, desired_tbv_dataroot: Path) -> None: +def untar_tbv_dataset( + num_workers: int, shard_dirpath: Path, desired_tbv_dataroot: Path +) -> None: """Untar each of the tar.gz archives. Args: @@ -58,7 +64,9 @@ def untar_tbv_dataset(num_workers: int, shard_dirpath: Path, desired_tbv_dataroo run_command(cmd) -@click.command(help="Extract TbV tar.gz archives that were previously downloaded to a local disk.") +@click.command( + help="Extract TbV tar.gz archives that were previously downloaded to a local disk." +) @click.option( "--num-workers", required=True, @@ -77,10 +85,14 @@ def untar_tbv_dataset(num_workers: int, shard_dirpath: Path, desired_tbv_dataroo help="Path to local directory, where TbV logs will be extracted.", type=str, ) -def run_untar_tbv_dataset(num_workers: int, shard_dirpath: str, desired_tbv_dataroot: str) -> None: +def run_untar_tbv_dataset( + num_workers: int, shard_dirpath: str, desired_tbv_dataroot: str +) -> None: """Click entry point for TbV tar.gz file extraction.""" untar_tbv_dataset( - num_workers=num_workers, shard_dirpath=Path(shard_dirpath), desired_tbv_dataroot=Path(desired_tbv_dataroot) + num_workers=num_workers, + shard_dirpath=Path(shard_dirpath), + desired_tbv_dataroot=Path(desired_tbv_dataroot), )